From 3b44ec6291bd15dbe6949858fbe2b920560bd280 Mon Sep 17 00:00:00 2001 From: asoseil Date: Mon, 20 Nov 2017 00:52:31 +0100 Subject: [PATCH] 2.0 raw --- assets/index.html | 9 + bindata.go | 2 +- cli.go | 1144 ++++++++++++++++++++++++++++++++++++++++ cmd.go | 157 ------ cmd_test.go | 156 ------ commands.go | 55 ++ exec.go | 228 -------- exec_test.go | 29 -- projects.go | 529 +++++++++++++++++++ realize.go | 1217 +++---------------------------------------- realize_test.go | 34 -- schema.go | 76 +++ server.go | 255 +++++---- server_test.go | 60 --- settings.go | 81 ++- settings_test.go | 114 ---- settings_unix.go | 2 +- settings_windows.go | 2 +- tools.go | 160 ++++++ utils.go | 2 +- watcher.go | 532 ------------------- watcher_test.go | 72 --- 22 files changed, 2207 insertions(+), 2709 deletions(-) create mode 100644 assets/index.html create mode 100644 cli.go delete mode 100644 cmd.go delete mode 100644 cmd_test.go create mode 100644 commands.go delete mode 100644 exec.go delete mode 100644 exec_test.go create mode 100644 projects.go delete mode 100644 realize_test.go create mode 100644 schema.go delete mode 100644 server_test.go delete mode 100644 settings_test.go create mode 100644 tools.go delete mode 100644 watcher.go delete mode 100644 watcher_test.go diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..9ad0403 --- /dev/null +++ b/assets/index.html @@ -0,0 +1,9 @@ + + + + +Realize + + + + \ No newline at end of file diff --git a/bindata.go b/bindata.go index 34917ed..d430eb0 100644 --- a/bindata.go +++ b/bindata.go @@ -398,7 +398,7 @@ func assetsIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "assets/index.html", size: 936, mode: os.FileMode(420), modTime: time.Unix(1505914049, 0)} + info := bindataFileInfo{name: "assets/index.html", size: 936, mode: os.FileMode(420), modTime: time.Unix(1510563869, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..5b9795d --- /dev/null +++ b/cli.go @@ -0,0 +1,1144 @@ +package main + +import ( + "github.com/fatih/color" + "github.com/tockins/interact" + "gopkg.in/urfave/cli.v2" + "log" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +// Version print current version +func version() { + r := Realize{} + log.Println(r.Prefix(green.bold(version))) +} + +// Clean remove realize folder +func clean() (err error) { + r := Realize{} + if err := r.Settings.Remove(RDir); err != nil { + return err + } + log.Println(r.Prefix(green.bold("folder successfully removed"))) + return nil +} + +// Add a project to an existing config or create a new one +func add(c *cli.Context) (err error) { + r := Realize{} + // read a config if exist + err = r.Settings.Read(&r) + if err != nil { + return err + } + projects := len(r.Schema.Projects) + // create and add a new project + r.Schema.Add(r.Schema.New(c)) + if len(r.Schema.Projects) > projects { + // update config + err = r.Settings.Write(r) + if err != nil { + return err + } + log.Println(r.Prefix(green.bold("project successfully added"))) + } else { + log.Println(r.Prefix(green.bold("project can't be added"))) + } + return nil +} + +// Setup a new config step by step +func setup(c *cli.Context) (err error) { + r := Realize{} + interact.Run(&interact.Interact{ + Before: func(context interact.Context) error { + context.SetErr(red.bold("INVALID INPUT")) + context.SetPrfx(color.Output, yellow.regular("[")+time.Now().Format("15:04:05")+yellow.regular("]")+yellow.bold("[")+strings.ToUpper(RPrefix)+yellow.bold("]")) + return nil + }, + Questions: []*interact.Question{ + { + Before: func(d interact.Context) error { + if _, err := os.Stat(RDir + "/" + RFile); err != nil { + d.Skip() + } + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Would you want to overwrite existing " + magenta.bold(RPrefix) + " config?", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } else if val { + r := Realize{} + r.Server = Server{&r, false, false, Port, Host} + } + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Would you want to customize settings?", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef(0, green.regular("(os default)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[int]"), + Msg: "Set max number of open files (root required)", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Int() + if err != nil { + return d.Err() + } + r.Settings.FileLimit = int32(val) + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Force polling watcher?", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef(100, green.regular("(100ms)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[int]"), + Msg: "Set polling interval", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Int() + if err != nil { + return d.Err() + } + r.Settings.Legacy.Interval = time.Duration(int(val)) * time.Millisecond + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Settings.Legacy.Force = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable logging files", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Settings.Files.Errors = Resource{Name: FileErr, Status: val} + r.Settings.Files.Outputs = Resource{Name: FileOut, Status: val} + r.Settings.Files.Logs = Resource{Name: FileLog, Status: val} + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable web server", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef(Port, green.regular("("+strconv.Itoa(Port)+")")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[int]"), + Msg: "Server port", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Int() + if err != nil { + return d.Err() + } + r.Server.Port = int(val) + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(Host, green.regular("("+Host+")")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Server host", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Server.Host = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Open in current browser", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Server.Open = val + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Server.Status = val + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + _, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(true, green.regular("(y)")) + d.SetEnd("!") + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Would you want to " + magenta.regular("add a new project") + "? (insert '!' to stop)", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + if val { + r.Schema.Add(r.Schema.New(c)) + } + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef(wdir(), green.regular("("+wdir()+")")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Project name", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Name = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + dir := wdir() + d.SetDef(dir, green.regular("("+dir+")")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Project path", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Path = filepath.Clean(val) + return nil + }, + }, + + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go vet", + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Vet additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Vet.Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Vet.Args, val) + } + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Vet.Status = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go fmt", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Fmt additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Fmt.Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Fmt.Args, val) + } + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Fmt.Status = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go test", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Test additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Test.Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Test.Args, val) + } + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Test.Status = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go fix", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Fix additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Fix.Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Fix.Args, val) + } + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Fix.Status = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go clean", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Clean additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Clean.Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Clean.Args, val) + } + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Clean.Status = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go generate", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Generate additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Generate.Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Generate.Args, val) + } + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Generate.Status = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(true, green.regular("(y)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go install", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Install additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Install.Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Install.Args, val) + } + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Install.Status = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go build", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Build additional arguments", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + if val != "" { + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Build.Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Build.Args, val) + } + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Build.Status = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(true, green.regular("(y)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Enable go run", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Tools.Run = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Customize watching paths", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + if val { + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Paths = r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Paths[:len(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Paths)-1] + } + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetEnd("!") + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Insert a path to watch (insert '!' to stop)", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Paths = append(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Paths, val) + d.Reload() + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + _, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Customize ignore paths", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + if val { + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Ignore = r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Ignore[:len(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Ignore)-1] + } + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetEnd("!") + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Insert a path to ignore (insert '!' to stop)", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Ignore = append(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Ignore, val) + d.Reload() + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + _, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Add an additional argument", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + d.SetEnd("!") + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Add another argument (insert '!' to stop)", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Args = append(r.Schema.Projects[len(r.Schema.Projects)-1].Args, val) + d.Reload() + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + _, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(none)")) + d.SetEnd("!") + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Add a 'before' custom command (insert '!' to stop)", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Insert a command", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts = append(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts, Command{Type: "before", Cmd: val}) + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Launch from a specific path", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts[len(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts)-1].Path = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Tag as global command", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts[len(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts)-1].Global = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Display command output", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts[len(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts)-1].Output = val + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + if val { + d.Reload() + } + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(none)")) + d.SetEnd("!") + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Add an 'after' custom commands (insert '!' to stop)", + Resolve: func(d interact.Context) bool { + val, _ := d.Ans().Bool() + return val + }, + }, + Subs: []*interact.Question{ + { + Before: func(d interact.Context) error { + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Insert a command", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts = append(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts, Command{Type: "after", Cmd: val}) + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Launch from a specific path", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts[len(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts)-1].Path = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Tag as global command", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts[len(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts)-1].Global = val + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef(false, green.regular("(n)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[y/n]"), + Msg: "Display command output", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts[len(r.Schema.Projects[len(r.Schema.Projects)-1].Watcher.Scripts)-1].Output = val + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().Bool() + if err != nil { + return d.Err() + } + if val { + d.Reload() + } + return nil + }, + }, + { + Before: func(d interact.Context) error { + d.SetDef("", green.regular("(none)")) + return nil + }, + Quest: interact.Quest{ + Options: yellow.regular("[string]"), + Msg: "Set an error output pattern", + }, + Action: func(d interact.Context) interface{} { + val, err := d.Ans().String() + if err != nil { + return d.Err() + } + r.Schema.Projects[len(r.Schema.Projects)-1].ErrorOutputPattern = val + return nil + }, + }, + }, + Action: func(d interact.Context) interface{} { + if val, err := d.Ans().Bool(); err != nil { + return d.Err() + } else if val { + d.Reload() + } + return nil + }, + }, + }, + After: func(d interact.Context) error { + if val, _ := d.Qns().Get(0).Ans().Bool(); val { + err := r.Settings.Remove(RDir) + if err != nil { + return err + } + } + return nil + }, + }) + // create config + err = r.Settings.Write(r) + if err != nil { + return err + } + log.Println(r.Prefix(green.bold("Config successfully created"))) + return nil +} + +// Start realize workflow +func start(c *cli.Context) (err error) { + r := Realize{} + r.Server = Server{&r, false, false, Port, Host} + // check no-config and read + if !c.Bool("no-config") { + // read a config if exist + err = r.Settings.Read(&r) + if err != nil { + return err + } + if c.String("name") != "" { + // filter by name flag if exist + r.Schema.Filter("name", c.String("name")) + } + // increase file limit + if r.Settings.FileLimit != 0 { + if err = r.Settings.Flimit(); err != nil { + return err + } + } + + } + // check project list length + if len(r.Schema.Projects) <= 0 { + // create a new project based on given params + project := r.Schema.New(c) + // Add to projects list + r.Schema.Add(project) + } + // save config + if !c.Bool("no-config") { + err = r.Settings.Write(r) + if err != nil { + return err + } + } + // config and start server + if c.Bool("server") || r.Server.Status { + r.Server.Status = true + if c.Bool("open") || r.Server.Open { + r.Server.Open = true + r.Server.OpenURL() + } + err = r.Server.Start() + if err != nil { + return err + } + } + // start workflow + r.Start() + return +} + +// Remove a project from an existing config +func remove(c *cli.Context) (err error) { + r := Realize{} + // read a config if exist + err = r.Settings.Read(&r) + if err != nil { + return err + } + if c.String("name") != "" { + err := r.Schema.Remove(c.String("name")) + if err != nil { + return err + } + // update config + err = r.Settings.Write(r) + if err != nil { + return err + } + log.Println(r.Prefix(green.bold("project successfully removed"))) + } else { + log.Println(r.Prefix(green.bold("project name not found"))) + } + return nil +} diff --git a/cmd.go b/cmd.go deleted file mode 100644 index 8a71296..0000000 --- a/cmd.go +++ /dev/null @@ -1,157 +0,0 @@ -package main - -import ( - "errors" - "gopkg.in/urfave/cli.v2" - "os" - "path/filepath" -) - -// Tool options customizable, should be moved in Cmd -type tool struct { - name, err, out string - cmd, options []string - dir, status bool -} - -// Cmds list of go commands -type Cmds struct { - Fix Cmd `yaml:"fix,omitempty" json:"fix,omitempty"` - Clean Cmd `yaml:"clean,omitempty" json:"clean,omitempty"` - Vet Cmd `yaml:"vet,omitempty" json:"vet,omitempty"` - Fmt Cmd `yaml:"fmt,omitempty" json:"fmt,omitempty"` - Test Cmd `yaml:"test,omitempty" json:"test,omitempty"` - Generate Cmd `yaml:"generate,omitempty" json:"generate,omitempty"` - Install Cmd `yaml:"install,omitempty" json:"install,omitempty"` - Build Cmd `yaml:"build,omitempty" json:"build,omitempty"` - Run bool `yaml:"run,omitempty" json:"run,omitempty"` -} - -// Cmd single command fields and options -type Cmd struct { - Method string `yaml:"method,omitempty" json:"method,omitempty"` - Args []string `yaml:"args,omitempty" json:"args,omitempty"` - Status bool `yaml:"status,omitempty" json:"status,omitempty"` - tool bool - method []string - name, startTxt, endTxt string -} - -// Clean duplicate projects -func (r *realize) clean() error { - if len(r.Schema) > 0 { - arr := r.Schema - for key, val := range arr { - if _, err := duplicates(val, arr[key+1:]); err != nil { - // path validation - - r.Schema = append(arr[:key], arr[key+1:]...) - break - } - } - return nil - } - return errors.New("there are no projects") -} - -// Add a new project -func (r *realize) add(p *cli.Context) (err error) { - // project init - name := filepath.Base(p.String("path")) - if name == "." { - name = filepath.Base(wdir()) - } - project := Project{ - Name: name, - Path: p.String("path"), - Cmds: Cmds{ - Vet: Cmd{ - Status: p.Bool("vet"), - }, - Fmt: Cmd{ - Status: p.Bool("fmt"), - }, - Test: Cmd{ - Status: p.Bool("test"), - }, - Generate: Cmd{ - Status: p.Bool("generate"), - }, - Build: Cmd{ - Status: p.Bool("build"), - }, - Install: Cmd{ - Status: p.Bool("install"), - }, - Run: p.Bool("run"), - }, - Args: params(p), - Watcher: Watch{ - Paths: []string{"/"}, - Ignore: []string{".git", ".realize", "vendor"}, - Exts: []string{"go"}, - }, - } - if _, err := duplicates(project, r.Schema); err != nil { - return err - } - r.Schema = append(r.Schema, project) - return nil -} - -// Run launches the toolchain for each project -func (r *realize) run(p *cli.Context) error { - var match bool - // check projects and remove duplicates - if err := r.clean(); err != nil { - return err - } - // set gobin - if err := os.Setenv("GOBIN", filepath.Join(os.Getenv("GOPATH"), "bin")); err != nil { - return err - } - // loop projects - if p.String("name") != "" { - wg.Add(1) - } else { - wg.Add(len(r.Schema)) - } - for k, elm := range r.Schema { - // command start using name flag - if p.String("name") != "" && elm.Name != p.String("name") { - continue - } - match = true - r.Schema[k].config(r) - go r.Schema[k].watch() - } - if !match { - return errors.New("there is no project with the given name") - } - wg.Wait() - return nil -} - -// Remove a project -func (r *realize) remove(p *cli.Context) error { - for key, val := range r.Schema { - if p.String("name") == val.Name { - r.Schema = append(r.Schema[:key], r.Schema[key+1:]...) - return nil - } - } - return errors.New("no project found") -} - -// Insert current project if there isn't already one -func (r *realize) insert(c *cli.Context) error { - if c.Bool("no-config") { - r.Schema = []Project{} - } - if len(r.Schema) <= 0 { - if err := r.add(c); err != nil { - return err - } - } - return nil -} diff --git a/cmd_test.go b/cmd_test.go deleted file mode 100644 index 3ebe2d8..0000000 --- a/cmd_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package main - -import ( - "flag" - "gopkg.in/urfave/cli.v2" - "log" - "os" - "path/filepath" - "reflect" - "testing" - "time" -) - -type loggerT struct{} - -func (loggerT) Write(bytes []byte) (int, error) { - return 0, nil -} - -func TestMain(m *testing.M) { - log.SetFlags(0) - log.SetOutput(loggerT{}) - os.Exit(m.Run()) -} - -func TestRealize_Clean(t *testing.T) { - r := realize{} - r.Schema = append(r.Schema, Project{Name: "test0"}) - r.Schema = append(r.Schema, Project{Name: "test0"}) - r.clean() - if len(r.Schema) > 1 { - t.Error("Expected only one project") - } - r.Schema = append(r.Schema, Project{Path: "test1"}) - r.Schema = append(r.Schema, Project{Path: "test1"}) - r.clean() - if len(r.Schema) != 2 { - t.Error("Expected two projects") - } - -} - -func TestRealize_Add(t *testing.T) { - r := realize{} - // add all flags, test with expected - set := flag.NewFlagSet("test", 0) - set.Bool("fmt", false, "") - set.Bool("vet", false, "") - set.Bool("test", false, "") - set.Bool("install", false, "") - set.Bool("run", false, "") - set.Bool("build", false, "") - set.Bool("generate", false, "") - set.String("path", wdir(), "") - c := cli.NewContext(nil, set, nil) - set.Parse([]string{"--fmt", "--install", "--run", "--build", "--generate", "--test", "--vet"}) - r.add(c) - expected := Project{ - Name: filepath.Base(wdir()), - Path: wdir(), - Cmds: Cmds{ - Fmt: Cmd{ - Status: true, - }, - Install: Cmd{ - Status: true, - }, - Generate: Cmd{ - Status: true, - }, - Test: Cmd{ - Status: true, - }, - Build: Cmd{ - Status: true, - }, - Vet: Cmd{ - Status: true, - }, - Run: true, - }, - Watcher: Watch{ - Paths: []string{"/"}, - Ignore: []string{".git", ".realize", "vendor"}, - Exts: []string{"go"}, - }, - } - if !reflect.DeepEqual(r.Schema[0], expected) { - t.Error("Expected equal struct") - } -} - -func TestRealize_Run(t *testing.T) { - set := flag.NewFlagSet("test", 0) - params := cli.NewContext(nil, set, nil) - m := make(map[string]string) - m["test"] = "test" - r := realize{} - r.Schema = []Project{ - { - Name: "test0", - Path: ".", - }, - { - Name: "test1", - Path: "/test", - }, - { - Name: "test2", - Path: "/test", - }, - } - go r.run(params) - time.Sleep(1 * time.Second) -} - -func TestRealize_Remove(t *testing.T) { - r := realize{} - set := flag.NewFlagSet("name", 0) - set.String("name", "", "") - c := cli.NewContext(nil, set, nil) - set.Parse([]string{"--name=test0"}) - err := r.remove(c) - if err == nil { - t.Error("Expected an error, there are no projects") - } - // Append a new project - r.Schema = append(r.Schema, Project{Name: "test0"}) - err = r.remove(c) - if err != nil { - t.Error("Error unexpected, the project should be remove", err) - } -} - -func TestRealize_Insert(t *testing.T) { - r := realize{} - // add all flags, test with expected - set := flag.NewFlagSet("test", 0) - set.Bool("no-config", false, "") - c := cli.NewContext(nil, set, nil) - set.Parse([]string{"--no-config"}) - - r.insert(c) - if len(r.Schema) != 1 { - t.Error("Expected one project instead", len(r.Schema)) - } - - r.Schema = []Project{} - r.Schema = append(r.Schema, Project{}) - r.Schema = append(r.Schema, Project{}) - c = cli.NewContext(nil, set, nil) - r.insert(c) - if len(r.Schema) != 1 { - t.Error("Expected one project instead", len(r.Schema)) - } -} diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..3d66c90 --- /dev/null +++ b/commands.go @@ -0,0 +1,55 @@ +package main + +import ( + "bytes" + "github.com/go-siris/siris/core/errors" + "os/exec" + "path/filepath" + "strings" +) + +// Command options +type Command struct { + Type string `yaml:"type" json:"type"` + Cmd string `yaml:"command" json:"command"` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + Global bool `yaml:"global,omitempty" json:"global,omitempty"` + Output bool `yaml:"output,omitempty" json:"output,omitempty"` +} + +// Exec an additional command from a defined path if specified +func (c *Command) Exec(base string, stop <-chan bool) (response Response) { + var stdout bytes.Buffer + var stderr bytes.Buffer + done := make(chan error) + args := strings.Split(strings.Replace(strings.Replace(c.Cmd, "'", "", -1), "\"", "", -1), " ") + ex := exec.Command(args[0], args[1:]...) + ex.Dir = base + // make cmd path + if c.Path != "" { + if strings.Contains(c.Path, base) { + ex.Dir = c.Path + } else { + ex.Dir = filepath.Join(base, c.Path) + } + } + ex.Stdout = &stdout + ex.Stderr = &stderr + // Start command + ex.Start() + go func() { done <- ex.Wait() }() + // Wait a result + select { + case <-stop: + // Stop running command + ex.Process.Kill() + case err := <-done: + // Command completed + response.Name = c.Cmd + response.Out = stdout.String() + if err != nil { + response.Err = errors.New(stderr.String()) + } + } + return +} diff --git a/exec.go b/exec.go deleted file mode 100644 index 09c4615..0000000 --- a/exec.go +++ /dev/null @@ -1,228 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "fmt" - "github.com/pkg/errors" - "log" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "sync" - "time" -) - -// GoCompile is used for compile a project -func (p *Project) goCompile(stop <-chan bool, method []string, args []string) (string, error) { - var out bytes.Buffer - var stderr bytes.Buffer - done := make(chan error) - args = append(method, args...) - cmd := exec.Command(args[0], args[1:]...) - cmd.Dir = p.Path - cmd.Stdout = &out - cmd.Stderr = &stderr - // Start command - cmd.Start() - go func() { done <- cmd.Wait() }() - // Wait a result - select { - case <-stop: - // Stop running command - cmd.Process.Kill() - return msgStop, nil - case err := <-done: - // Command completed - if err != nil { - return stderr.String(), err - } - return "", nil - } - return "", nil -} - -// GoRun is an implementation of the bin execution -func (p *Project) goRun(stop <-chan bool, runner chan bool) { - var build *exec.Cmd - var args []string - // custom error pattern - isErrorText := func(string) bool { - return false - } - errRegexp, err := regexp.Compile(p.ErrorOutputPattern) - if err != nil { - msg := fmt.Sprintln(p.pname(p.Name, 3), ":", blue.regular(err.Error())) - out := BufferOut{Time: time.Now(), Text: err.Error(), Type: "Go Run"} - p.stamp("error", out, msg, "") - } else { - isErrorText = func(t string) bool { - return errRegexp.MatchString(t) - } - } - - // add additional arguments - for _, arg := range p.Args { - a := strings.FieldsFunc(arg, func(i rune) bool { - return i == '"' || i == '=' || i == '\'' - }) - args = append(args, a...) - } - - gobin := os.Getenv("GOBIN") - dirPath := filepath.Base(p.Path) - if p.Path == "." { - dirPath = filepath.Base(wdir()) - } - path := filepath.Join(gobin, dirPath) - if _, err := os.Stat(path); err == nil { - build = exec.Command(path, args...) - } else if _, err := os.Stat(path + extWindows); err == nil { - build = exec.Command(path+extWindows, args...) - } else { - if _, err = os.Stat(path); err == nil { - build = exec.Command(path, args...) - } else if _, err = os.Stat(path + extWindows); err == nil { - build = exec.Command(path+extWindows, args...) - } else { - p.err(errors.New("Build not found")) - return - } - } - - defer func() { - if err := build.Process.Kill(); err != nil { - p.Buffer.StdLog = append(p.Buffer.StdLog, BufferOut{Time: time.Now(), Text: "Failed to stop: " + err.Error()}) - p.fatal(err, "Failed to stop", ":") - } - msg := fmt.Sprintln(p.pname(p.Name, 2), ":", red.regular("Ended")) - out := BufferOut{Time: time.Now(), Text: "Ended", Type: "Go Run"} - p.stamp("log", out, msg, "") - }() - - // scan project stream - stdout, err := build.StdoutPipe() - stderr, err := build.StderrPipe() - if err != nil { - log.Println(red.bold(err.Error())) - return - } - if err := build.Start(); err != nil { - log.Println(red.bold(err.Error())) - return - } - close(runner) - - execOutput, execError := bufio.NewScanner(stdout), bufio.NewScanner(stderr) - stopOutput, stopError := make(chan bool, 1), make(chan bool, 1) - scanner := func(stop chan bool, output *bufio.Scanner, isError bool) { - for output.Scan() { - text := output.Text() - msg := fmt.Sprintln(p.pname(p.Name, 3), ":", blue.regular(text)) - if isError && !isErrorText(text) { - out := BufferOut{Time: time.Now(), Text: text, Type: "Go Run"} - p.stamp("error", out, msg, "") - } else { - out := BufferOut{Time: time.Now(), Text: text, Type: "Go Run"} - p.stamp("out", out, msg, "") - } - } - close(stop) - } - go scanner(stopOutput, execOutput, false) - go scanner(stopError, execError, true) - for { - select { - case <-stop: - return - case <-stopOutput: - return - case <-stopError: - return - } - } -} - -// Exec an additional command from a defined path if specified -func (p *Project) command(stop <-chan bool, cmd Command) (string, string) { - var stdout bytes.Buffer - var stderr bytes.Buffer - done := make(chan error) - args := strings.Split(strings.Replace(strings.Replace(cmd.Command, "'", "", -1), "\"", "", -1), " ") - ex := exec.Command(args[0], args[1:]...) - ex.Dir = p.Path - // make cmd path - if cmd.Path != "" { - if strings.Contains(cmd.Path, p.Path) { - ex.Dir = cmd.Path - } else { - ex.Dir = filepath.Join(p.Path, cmd.Path) - } - } - ex.Stdout = &stdout - ex.Stderr = &stderr - // Start command - ex.Start() - go func() { done <- ex.Wait() }() - // Wait a result - select { - case <-stop: - // Stop running command - ex.Process.Kill() - return "", "" - case err := <-done: - // Command completed - if err != nil { - return stderr.String(), stdout.String() - } - } - return "", stdout.String() -} - -// GoTool is used for run go tools methods such as fmt, test, generate and so on -func (p *Project) goTool(wg *sync.WaitGroup, stop <-chan bool, result chan<- tool, path string, tool tool) { - defer wg.Done() - if tool.status { - if tool.dir && filepath.Ext(path) != "" { - path = filepath.Dir(path) - } - if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "") { - if strings.HasSuffix(path, ".go") { - tool.options = append(tool.options, path) - path = p.Path - } - if s := ext(path); s == "" || s == "go" { - var out, stderr bytes.Buffer - done := make(chan error) - tool.cmd = append(tool.cmd, tool.options...) - cmd := exec.Command(tool.cmd[0], tool.cmd[1:]...) - cmd.Dir = path - cmd.Stdout = &out - cmd.Stderr = &stderr - // Start command - cmd.Start() - go func() { done <- cmd.Wait() }() - // Wait a result - select { - case <-stop: - // Stop running command - cmd.Process.Kill() - return - case err := <-done: - // Command completed - if err != nil { - tool.err = stderr.String() + out.String() - // send command result - result <- tool - } else { - tool.out = out.String() - } - return - } - - } - } - } -} diff --git a/exec_test.go b/exec_test.go deleted file mode 100644 index c7613e9..0000000 --- a/exec_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "testing" - "time" -) - -func TestProject_GoCompile(t *testing.T) { - p := Project{} - stop := make(chan bool) - response := make(chan string) - result, err := p.goCompile(stop, []string{"echo"}, []string{"test"}) - if err != nil { - t.Error("Unexpected", err) - } - go func() { - result, _ = p.goCompile(stop, []string{"sleep"}, []string{"20s"}) - response <- result - }() - close(stop) - select { - case v := <-response: - if v != msgStop { - t.Error("Unexpected result", response) - } - case <-time.After(2 * time.Second): - t.Error("Channel doesn't works") - } -} diff --git a/projects.go b/projects.go new file mode 100644 index 0000000..c6d8b90 --- /dev/null +++ b/projects.go @@ -0,0 +1,529 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "github.com/fsnotify/fsnotify" + "log" + "math/big" + "os" + "os/exec" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +var ( + msg string + out BufferOut +) + +// Watch info +type Watch struct { + Paths []string `yaml:"paths" json:"paths"` + Exts []string `yaml:"extensions" json:"extensions"` + Ignore []string `yaml:"ignored_paths,omitempty" json:"ignored_paths,omitempty"` + Scripts []Command `yaml:"scripts,omitempty" json:"scripts,omitempty"` +} + +// Project info +type Project struct { + parent *Realize + watcher FileWatcher + init bool + files int64 + folders int64 + name string + lastFile string + paths []string + lastTime time.Time + Name string `yaml:"name" json:"name"` + Path string `yaml:"path" json:"path"` + Environment map[string]string `yaml:"environment,omitempty" json:"environment,omitempty"` + Tools Tools `yaml:"commands" json:"commands"` + Args []string `yaml:"args,omitempty" json:"args,omitempty"` + Watcher Watch `yaml:"watcher" json:"watcher"` + Buffer Buffer `yaml:"-" json:"buffer"` + ErrorOutputPattern string `yaml:"errorOutputPattern,omitempty" json:"errorOutputPattern,omitempty"` +} + +// Response exec +type Response struct { + Name string + Out string + Err error +} + +// Buffer define an array buffer for each log files +type Buffer struct { + StdOut []BufferOut `json:"stdOut"` + StdLog []BufferOut `json:"stdLog"` + StdErr []BufferOut `json:"stdErr"` +} + +// BufferOut is used for exchange information between "realize cli" and "web realize" +type BufferOut struct { + Time time.Time `json:"time"` + Text string `json:"text"` + Path string `json:"path"` + Type string `json:"type"` + Stream string `json:"stream"` + Errors []string `json:"errors"` +} + +// Setup a project +func (p *Project) Setup() { + // get base path + p.name = filepath.Base(p.Path) + // set env const + for key, item := range p.Environment { + if err := os.Setenv(key, item); err != nil { + p.Buffer.StdErr = append(p.Buffer.StdErr, BufferOut{Time: time.Now(), Text: err.Error(), Type: "Env error", Stream: ""}) + } + } + p.Tools.Setup() +} + +// Watch a project +func (p *Project) Watch(exit chan os.Signal) { + var err error + stop := make(chan bool) + // init a new watcher + p.watcher, err = Watcher(p.parent.Settings.Legacy.Force, p.parent.Settings.Legacy.Interval) + if err != nil { + log.Fatal(err) + } + // global commands before + p.cmd(stop, "before", true) + // indexing files and dirs + for _, dir := range p.Watcher.Paths { + base, _ := filepath.Abs(p.Path) + base = filepath.Join(base, dir) + if _, err := os.Stat(base); err == nil { + if err := filepath.Walk(base, p.walk); err == nil { + p.tools(stop, base) + } + } else { + p.err(err) + } + } + // start message + msg = fmt.Sprintln(p.pname(p.Name, 1), ":", blue.bold("Watching"), magenta.bold(p.files), "file/s", magenta.bold(p.folders), "folder/s") + out = BufferOut{Time: time.Now(), Text: "Watching " + strconv.FormatInt(p.files, 10) + " files/s " + strconv.FormatInt(p.folders, 10) + " folder/s"} + p.stamp("log", out, msg, "") + // start watcher + go p.Reload(p.watcher, "", stop) +L: + for { + select { + case event := <-p.watcher.Events(): + if time.Now().Truncate(time.Second).After(p.lastTime) || event.Name != p.lastFile { + // event time + eventTime := time.Now() + // file extension + ext := ext(event.Name) + if ext == "" { + ext = "DIR" + } + // change message + msg = fmt.Sprintln(p.pname(p.Name, 4), ":", magenta.bold(strings.ToUpper(ext)), "changed", magenta.bold(event.Name)) + out = BufferOut{Time: time.Now(), Text: ext + " changed " + event.Name} + // switch event type + switch event.Op { + case fsnotify.Chmod: + case fsnotify.Remove: + p.watcher.Remove(event.Name) + if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) { + close(stop) + stop = make(chan bool) + p.stamp("log", out, msg, "") + go p.Reload(p.watcher, "", stop) + } + default: + file, err := os.Stat(event.Name) + if err != nil { + continue + } + if file.IsDir() { + filepath.Walk(event.Name, p.walk) + } else if file.Size() > 0 { + if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) { + // change watched + // check if a file is still writing #119 + if event.Op != fsnotify.Write || (eventTime.Truncate(time.Millisecond).After(file.ModTime().Truncate(time.Millisecond)) || event.Name != p.lastFile) { + close(stop) + stop = make(chan bool) + // stop and start again + p.stamp("log", out, msg, "") + go p.Reload(p.watcher, event.Name, stop) + } + } + p.lastTime = time.Now().Truncate(time.Second) + p.lastFile = event.Name + } + } + } + case err := <-p.watcher.Errors(): + p.err(err) + case <-exit: + p.cmd(nil, "after", true) + break L + } + } +} + +// Reload launches the toolchain run, build, install +func (p *Project) Reload(watcher FileWatcher, path string, stop <-chan bool) { + var done bool + var install, build Response + go func() { + for { + select { + case <-stop: + done = true + return + } + } + }() + if done { + return + } + // before command + p.cmd(stop, "before", false) + if done { + return + } + // Go supported tools + p.tools(stop, path) + // Prevent fake events on polling startup + p.init = true + // prevent errors using realize without config with only run flag + if p.Tools.Run && !p.Tools.Install.Status && !p.Tools.Build.Status { + p.Tools.Install.Status = true + } + if done { + return + } + if p.Tools.Install.Status { + msg = fmt.Sprintln(p.pname(p.Name, 1), ":", green.regular(p.Tools.Install.name), "started") + out = BufferOut{Time: time.Now(), Text: p.Tools.Install.name + " started"} + p.stamp("log", out, msg, "") + start := time.Now() + install = p.Tools.Install.Compile(p.Path, stop) + install.printAfter(start, p) + } + if done { + return + } + if p.Tools.Build.Status { + msg = fmt.Sprintln(p.pname(p.Name, 1), ":", green.regular(p.Tools.Build.name), "started") + out = BufferOut{Time: time.Now(), Text: p.Tools.Build.name + " started"} + p.stamp("log", out, msg, "") + start := time.Now() + build = p.Tools.Build.Compile(p.Path, stop) + build.printAfter(start, p) + } + if done { + return + } + if install.Err == nil && build.Err == nil && p.Tools.Run { + var start time.Time + result := make(chan Response) + go func() { + select { + case r := <-result: + if r.Err != nil { + msg := fmt.Sprintln(p.pname(p.Name, 2), ":", red.regular(r.Err)) + out := BufferOut{Time: time.Now(), Text: r.Err.Error(), Type: "Go Run"} + p.stamp("error", out, msg, "") + } + if r.Out != "" { + msg := fmt.Sprintln(p.pname(p.Name, 3), ":", blue.regular(r.Out)) + out := BufferOut{Time: time.Now(), Text: r.Out, Type: "Go Run"} + p.stamp("out", out, msg, "") + } + } + }() + go func() { + log.Println(p.pname(p.Name, 1), ":", "Running..") + start = time.Now() + p.Run(p.Path, stop) + }() + } + if done { + return + } + p.cmd(stop, "after", false) +} + +// Run a project +func (p *Project) Run(path string, stop <-chan bool) (response chan Response) { + var args []string + var build *exec.Cmd + var r Response + defer func() { + if err := build.Process.Kill(); err != nil { + r.Err = err + } + }() + + // custom error pattern + isErrorText := func(string) bool { + return false + } + errRegexp, err := regexp.Compile(p.ErrorOutputPattern) + if err != nil { + r.Err = err + response <- r + r.Err = nil + } else { + isErrorText = func(t string) bool { + return errRegexp.MatchString(t) + } + } + + // add additional arguments + for _, arg := range p.Args { + a := strings.FieldsFunc(arg, func(i rune) bool { + return i == '"' || i == '=' || i == '\'' + }) + args = append(args, a...) + } + gobin := os.Getenv("GOBIN") + dirPath := filepath.Base(path) + if path == "." { + dirPath = filepath.Base(wdir()) + } + path = filepath.Join(gobin, dirPath) + if _, err := os.Stat(path); err == nil { + build = exec.Command(path, args...) + } else if _, err := os.Stat(path + RExtWin); err == nil { + build = exec.Command(path+RExtWin, args...) + } else { + if _, err = os.Stat(path); err == nil { + build = exec.Command(path, args...) + } else if _, err = os.Stat(path + RExtWin); err == nil { + build = exec.Command(path+RExtWin, args...) + } else { + r.Err = errors.New("project not found") + return + } + } + // scan project stream + stdout, err := build.StdoutPipe() + stderr, err := build.StderrPipe() + if err != nil { + r.Err = err + return + } + if err := build.Start(); err != nil { + r.Err = err + return + } + execOutput, execError := bufio.NewScanner(stdout), bufio.NewScanner(stderr) + stopOutput, stopError := make(chan bool, 1), make(chan bool, 1) + scanner := func(stop chan bool, output *bufio.Scanner, isError bool) { + for output.Scan() { + text := output.Text() + if isError && !isErrorText(text) { + r.Err = errors.New(text) + response <- r + r.Err = nil + } else { + r.Out = text + response <- r + r.Out = "" + } + } + close(stop) + } + go scanner(stopOutput, execOutput, false) + go scanner(stopError, execError, true) + for { + select { + case <-stop: + return + case <-stopOutput: + return + case <-stopError: + return + } + } +} + +// Error occurred +func (p *Project) err(err error) { + msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.regular(err.Error())) + out = BufferOut{Time: time.Now(), Text: err.Error()} + p.stamp("error", out, msg, "") +} + +// Defines the colors scheme for the project name +func (p *Project) pname(name string, color int) string { + switch color { + case 1: + name = yellow.regular("[") + strings.ToUpper(name) + yellow.regular("]") + break + case 2: + name = yellow.regular("[") + red.bold(strings.ToUpper(name)) + yellow.regular("]") + break + case 3: + name = yellow.regular("[") + blue.bold(strings.ToUpper(name)) + yellow.regular("]") + break + case 4: + name = yellow.regular("[") + magenta.bold(strings.ToUpper(name)) + yellow.regular("]") + break + case 5: + name = yellow.regular("[") + green.bold(strings.ToUpper(name)) + yellow.regular("]") + break + } + return name +} + +// Tool logs the result of a go command +func (p *Project) tools(stop <-chan bool, path string) { + if len(path) > 0 { + done := make(chan bool) + result := make(chan Response) + v := reflect.ValueOf(p.Tools) + go func() { + for i := 0; i < v.NumField()-1; i++ { + tool := v.Field(i).Interface().(Tool) + if tool.Status && tool.isTool { + result <- tool.Exec(path, stop) + } + } + close(done) + }() + for { + select { + case <-done: + return + case <-stop: + return + case r := <-result: + if r.Err != nil { + msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(r.Name), red.regular("there are some errors in"), ":", magenta.bold(path)) + buff := BufferOut{Time: time.Now(), Text: "there are some errors in", Path: path, Type: r.Name, Stream: r.Err.Error()} + p.stamp("error", buff, msg, r.Err.Error()) + } else if r.Out != "" { + msg = fmt.Sprintln(p.pname(p.Name, 3), ":", red.bold(r.Name), red.regular("outputs"), ":", blue.bold(path)) + buff := BufferOut{Time: time.Now(), Text: "outputs", Path: path, Type: r.Name, Stream: r.Out} + p.stamp("out", buff, msg, r.Out) + } + } + } + } +} + +// Cmd after/before +func (p *Project) cmd(stop <-chan bool, flag string, global bool) { + done := make(chan bool) + result := make(chan Response) + // commands sequence + go func() { + for _, cmd := range p.Watcher.Scripts { + if strings.ToLower(cmd.Type) == flag && cmd.Global == global { + result <- cmd.Exec(p.Path, stop) + } + } + close(done) + }() + for { + select { + case <-stop: + return + case <-done: + return + case r := <-result: + msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.bold("Command"), green.bold("\"")+r.Name+green.bold("\"")) + out = BufferOut{Time: time.Now(), Text: r.Name, Type: flag} + if r.Err != nil { + p.stamp("error", out, msg, "") + out = BufferOut{Time: time.Now(), Text: r.Err.Error(), Type: flag} + p.stamp("error", out, "", fmt.Sprintln(red.regular(r.Err.Error()))) + } + if r.Out != "" { + out = BufferOut{Time: time.Now(), Text: r.Out, Type: flag} + p.stamp("log", out, "", fmt.Sprintln(r.Out)) + } else { + p.stamp("log", out, msg, "") + } + } + } +} + +// Watch the files tree of a project +func (p *Project) walk(path string, info os.FileInfo, err error) error { + for _, v := range p.Watcher.Ignore { + s := append([]string{p.Path}, strings.Split(v, string(os.PathSeparator))...) + if strings.Contains(path, filepath.Join(s...)) { + return nil + } + } + if !strings.HasPrefix(path, ".") && (info.IsDir() || array(ext(path), p.Watcher.Exts)) { + result := p.watcher.Walk(path, p.init) + if result != "" { + if info.IsDir() { + p.folders++ + } else { + p.files++ + } + } + } + return nil +} + +// Print on files, cli, ws +func (p *Project) stamp(t string, o BufferOut, msg string, stream string) { + time := time.Now() + content := []string{time.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n", stream} + switch t { + case "out": + p.Buffer.StdOut = append(p.Buffer.StdOut, o) + if p.parent.Settings.Files.Outputs.Status { + f := p.parent.Settings.Create(p.Path, p.parent.Settings.Files.Outputs.Name) + if _, err := f.WriteString(strings.Join(content, " ")); err != nil { + p.parent.Settings.Fatal(err, "") + } + } + case "log": + p.Buffer.StdLog = append(p.Buffer.StdLog, o) + if p.parent.Settings.Files.Logs.Status { + f := p.parent.Settings.Create(p.Path, p.parent.Settings.Files.Logs.Name) + if _, err := f.WriteString(strings.Join(content, " ")); err != nil { + p.parent.Settings.Fatal(err, "") + } + } + case "error": + p.Buffer.StdErr = append(p.Buffer.StdErr, o) + if p.parent.Settings.Files.Errors.Status { + f := p.parent.Settings.Create(p.Path, p.parent.Settings.Files.Errors.Name) + if _, err := f.WriteString(strings.Join(content, " ")); err != nil { + p.parent.Settings.Fatal(err, "") + } + } + } + if msg != "" { + log.Print(msg) + } + if stream != "" { + fmt.Fprint(output, stream) + } +} + +func (r *Response) printAfter(start time.Time, p *Project) { + if r.Err != nil { + msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(r.Name), red.regular(r.Err.Error())) + out = BufferOut{Time: time.Now(), Text: r.Err.Error(), Type: r.Name, Stream: r.Out} + p.stamp("error", out, msg, r.Out) + } else { + msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.bold(r.Name), "completed in", magenta.regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) + out = BufferOut{Time: time.Now(), Text: r.Name + " in " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} + p.stamp("log", out, msg, r.Out) + } +} diff --git a/realize.go b/realize.go index 5e33324..933cbc6 100644 --- a/realize.go +++ b/realize.go @@ -1,48 +1,61 @@ package main import ( - "errors" "fmt" - "github.com/fatih/color" - "github.com/tockins/interact" "go/build" "gopkg.in/urfave/cli.v2" "log" "os" + "os/signal" "path/filepath" - "strconv" + "strings" + "syscall" "time" ) const ( - version = "1.5.2" + RPrefix = "realize" + RVersion = "2.0" + RExt = ".yaml" + RFile = RPrefix + RExt + RDir = "." + RPrefix + RExtWin = ".exe" ) -// New realize instance -var r realize +type ( + Realize struct { + Settings Settings `yaml:"settings" json:"settings"` + Server Server `yaml:"server" json:"server"` + Schema `yaml:",inline"` + sync chan string + exit chan os.Signal + } + LogWriter struct{} +) -// Log struct -type logWriter struct{} - -// Realize struct contains the general app informations -type realize struct { - Settings Settings `yaml:"settings" json:"settings"` - Server Server `yaml:"server" json:"server"` - Schema []Project `yaml:"schema" json:"schema"` - sync chan string +func init() { + // custom log + log.SetFlags(0) + log.SetOutput(LogWriter{}) + if build.Default.GOPATH == "" { + log.Fatal("$GOPATH isn't set properly") + } + if err := os.Setenv("GOBIN", filepath.Join(build.Default.GOPATH, "bin")); err != nil { + log.Fatal(err) + } } -// Cli commands +// Realize cli commands func main() { app := &cli.App{ - Name: "Realize", - Version: version, - Description: "Go build system with file watchers, output streams and live reload. Run, build and watch file changes with custom paths", + Name: strings.Title(RPrefix), + Version: RVersion, + Description: "Realize is the #1 Golang Task Runner which enhance your workflow by automating the most common tasks and using the best performing Golang live reloading.", Commands: []*cli.Command{ { Name: "start", Aliases: []string{"s"}, - Description: "Start a toolchain on a project or a list of projects. If not exist a config file it creates a new one", + Description: "Start " + strings.Title(RPrefix) + " on a given path. If not exist a config file it creates a new one.", Flags: []cli.Flag{ &cli.StringFlag{Name: "path", Aliases: []string{"p"}, Value: ".", Usage: "Project base path"}, &cli.StringFlag{Name: "name", Aliases: []string{"n"}, Value: "", Usage: "Run a project by its name"}, @@ -56,27 +69,15 @@ func main() { &cli.BoolFlag{Name: "run", Aliases: []string{"nr"}, Value: false, Usage: "Enable go run"}, &cli.BoolFlag{Name: "no-config", Aliases: []string{"nc"}, Value: false, Usage: "Ignore existing config and doesn't create a new one"}, }, - Action: func(p *cli.Context) error { - if err := r.insert(p); err != nil { - return err - } - if !p.Bool("no-config") && p.String("name") == "" { - if err := r.Settings.record(r); err != nil { - return err - } - } - if err := r.Server.start(p); err != nil { - return err - } - return r.run(p) + Action: func(c *cli.Context) error { + return start(c) }, - Before: before, }, { Name: "add", Category: "Configuration", Aliases: []string{"a"}, - Description: "Add a project to an existing config file or create a new one", + Description: "Add a project to an existing config or to a new one.", Flags: []cli.Flag{ &cli.StringFlag{Name: "path", Aliases: []string{"p"}, Value: wdir(), Usage: "Project base path"}, &cli.BoolFlag{Name: "fmt", Aliases: []string{"f"}, Value: false, Usage: "Enable go fmt"}, @@ -87,1156 +88,88 @@ func main() { &cli.BoolFlag{Name: "build", Aliases: []string{"b"}, Value: false, Usage: "Enable go build"}, &cli.BoolFlag{Name: "run", Aliases: []string{"nr"}, Value: false, Usage: "Enable go run"}, }, - Action: func(p *cli.Context) error { - if err := r.add(p); err != nil { - return err - } - if err := r.Settings.record(r); err != nil { - return err - } - log.Println(prefix(green.bold("Your project was successfully added"))) - return nil + Action: func(c *cli.Context) error { + return add(c) }, - Before: before, }, { Name: "init", Category: "Configuration", Aliases: []string{"i"}, - Description: "Define a new config file with all options step by step", - Action: func(p *cli.Context) (actErr error) { - interact.Run(&interact.Interact{ - Before: func(context interact.Context) error { - context.SetErr(red.bold("INVALID INPUT")) - context.SetPrfx(color.Output, yellow.regular("[")+time.Now().Format("15:04:05")+yellow.regular("]")+yellow.bold("[")+"REALIZE"+yellow.bold("]")) - return nil - }, - Questions: []*interact.Question{ - { - Before: func(d interact.Context) error { - if _, err := os.Stat(directory + "/" + file); err != nil { - d.Skip() - } - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Would you want to overwrite existing " + magenta.bold("Realize") + " config?", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } else if val { - r = new() - } - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Would you want to customize settings?", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef(0, green.regular("(os default)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[int]"), - Msg: "Set max number of open files (root required)", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Int() - if err != nil { - return d.Err() - } - r.Settings.FileLimit = int32(val) - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Force polling watcher?", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef(100, green.regular("(100ms)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[int]"), - Msg: "Set polling interval", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Int() - if err != nil { - return d.Err() - } - r.Settings.Legacy.Interval = time.Duration(int(val)) * time.Millisecond - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Settings.Legacy.Force = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable logging files", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Settings.Files.Errors = Resource{Name: fileErr, Status: val} - r.Settings.Files.Outputs = Resource{Name: fileOut, Status: val} - r.Settings.Files.Logs = Resource{Name: fileLog, Status: val} - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable web server", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef(port, green.regular("("+strconv.Itoa(port)+")")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[int]"), - Msg: "Server port", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Int() - if err != nil { - return d.Err() - } - r.Server.Port = int(val) - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(host, green.regular("("+host+")")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Server host", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Server.Host = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Open in current browser", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Server.Open = val - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Server.Status = val - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - _, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(true, green.regular("(y)")) - d.SetEnd("!") - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Would you want to " + magenta.regular("add a new project") + "? (insert '!' to stop)", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - if val { - r.add(p) - } - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef(wdir(), green.regular("("+wdir()+")")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Project name", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Name = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - dir := wdir() - d.SetDef(dir, green.regular("("+dir+")")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Project path", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Path = filepath.Clean(val) - return nil - }, - }, - - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go vet", - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Vet additional arguments", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - if val != "" { - r.Schema[len(r.Schema)-1].Cmds.Vet.Args = append(r.Schema[len(r.Schema)-1].Cmds.Vet.Args, val) - } - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Vet.Status = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go fmt", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Fmt additional arguments", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - if val != "" { - r.Schema[len(r.Schema)-1].Cmds.Fmt.Args = append(r.Schema[len(r.Schema)-1].Cmds.Fmt.Args, val) - } - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Fmt.Status = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go test", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Test additional arguments", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - if val != "" { - r.Schema[len(r.Schema)-1].Cmds.Test.Args = append(r.Schema[len(r.Schema)-1].Cmds.Test.Args, val) - } - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Test.Status = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go fix", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Fix additional arguments", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - if val != "" { - r.Schema[len(r.Schema)-1].Cmds.Fix.Args = append(r.Schema[len(r.Schema)-1].Cmds.Fix.Args, val) - } - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Fix.Status = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go clean", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Clean additional arguments", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - if val != "" { - r.Schema[len(r.Schema)-1].Cmds.Clean.Args = append(r.Schema[len(r.Schema)-1].Cmds.Clean.Args, val) - } - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Clean.Status = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go generate", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Generate additional arguments", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - if val != "" { - r.Schema[len(r.Schema)-1].Cmds.Generate.Args = append(r.Schema[len(r.Schema)-1].Cmds.Generate.Args, val) - } - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Generate.Status = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(true, green.regular("(y)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go install", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Install additional arguments", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - if val != "" { - r.Schema[len(r.Schema)-1].Cmds.Install.Args = append(r.Schema[len(r.Schema)-1].Cmds.Install.Args, val) - } - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Install.Status = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go build", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Build additional arguments", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - if val != "" { - r.Schema[len(r.Schema)-1].Cmds.Build.Args = append(r.Schema[len(r.Schema)-1].Cmds.Build.Args, val) - } - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Build.Status = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(true, green.regular("(y)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Enable go run", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Cmds.Run = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Customize watching paths", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - if val { - r.Schema[len(r.Schema)-1].Watcher.Paths = r.Schema[len(r.Schema)-1].Watcher.Paths[:len(r.Schema[len(r.Schema)-1].Watcher.Paths)-1] - } - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetEnd("!") - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Insert a path to watch (insert '!' to stop)", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Paths = append(r.Schema[len(r.Schema)-1].Watcher.Paths, val) - d.Reload() - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - _, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Customize ignore paths", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - if val { - r.Schema[len(r.Schema)-1].Watcher.Ignore = r.Schema[len(r.Schema)-1].Watcher.Ignore[:len(r.Schema[len(r.Schema)-1].Watcher.Ignore)-1] - } - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetEnd("!") - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Insert a path to ignore (insert '!' to stop)", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Ignore = append(r.Schema[len(r.Schema)-1].Watcher.Ignore, val) - d.Reload() - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - _, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Add an additional argument", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - d.SetEnd("!") - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Add another argument (insert '!' to stop)", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Args = append(r.Schema[len(r.Schema)-1].Args, val) - d.Reload() - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - _, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(none)")) - d.SetEnd("!") - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Add a 'before' custom command (insert '!' to stop)", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Insert a command", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Scripts = append(r.Schema[len(r.Schema)-1].Watcher.Scripts, Command{Type: "before", Command: val}) - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Launch from a specific path", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Path = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Tag as global command", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Global = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Display command output", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Output = val - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - if val { - d.Reload() - } - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(none)")) - d.SetEnd("!") - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Add an 'after' custom commands (insert '!' to stop)", - Resolve: func(d interact.Context) bool { - val, _ := d.Ans().Bool() - return val - }, - }, - Subs: []*interact.Question{ - { - Before: func(d interact.Context) error { - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Insert a command", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Scripts = append(r.Schema[len(r.Schema)-1].Watcher.Scripts, Command{Type: "after", Command: val}) - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Launch from a specific path", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Path = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Tag as global command", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Global = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Display command output", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Scripts[len(r.Schema[len(r.Schema)-1].Watcher.Scripts)-1].Output = val - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - if val { - d.Reload() - } - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef(false, green.regular("(n)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[y/n]"), - Msg: "Print watched files on startup", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().Bool() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].Watcher.Preview = val - return nil - }, - }, - { - Before: func(d interact.Context) error { - d.SetDef("", green.regular("(none)")) - return nil - }, - Quest: interact.Quest{ - Options: yellow.regular("[string]"), - Msg: "Set an error output pattern", - }, - Action: func(d interact.Context) interface{} { - val, err := d.Ans().String() - if err != nil { - return d.Err() - } - r.Schema[len(r.Schema)-1].ErrorOutputPattern = val - return nil - }, - }, - }, - Action: func(d interact.Context) interface{} { - if val, err := d.Ans().Bool(); err != nil { - return d.Err() - } else if val { - d.Reload() - } - return nil - }, - }, - }, - After: func(d interact.Context) error { - if val, _ := d.Qns().Get(0).Ans().Bool(); val { - actErr = r.Settings.del(directory) - if actErr != nil { - return actErr - } - } - return nil - }, - }) - if err := r.Settings.record(r); err != nil { - return err - } - log.Println(prefix(green.bold("Your configuration was successful"))) - return nil + Description: "Make a new config file step by step.", + Action: func(c *cli.Context) error { + return setup(c) }, - Before: before, }, { Name: "remove", Category: "Configuration", Aliases: []string{"r"}, - Description: "Remove a project from a realize configuration", + Description: "Remove a project from an existing config.", Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Aliases: []string{"n"}, Value: ""}, }, - Action: func(p *cli.Context) error { - if err := r.remove(p); err != nil { - return err - } - if err := r.Settings.record(r); err != nil { - return err - } - log.Println(prefix(green.bold("Your project was successfully removed"))) - return nil + Action: func(c *cli.Context) error { + return remove(c) }, - Before: before, }, { Name: "clean", Category: "Configuration", Aliases: []string{"c"}, - Description: "Remove realize folder", - Action: func(p *cli.Context) error { - if err := r.Settings.del(directory); err != nil { - return err - } - log.Println(prefix(green.bold("Realize folder successfully removed"))) - return nil + Description: "Remove " + strings.Title(RPrefix) + " folder.", + Action: func(c *cli.Context) error { + return clean() }, - Before: before, }, { Name: "version", Aliases: []string{"v"}, - Description: "Realize version", + Description: "Print " + strings.Title(RPrefix) + " version.", Action: func(p *cli.Context) error { - log.Println(prefix(green.bold(version))) + version() return nil }, - Before: before, }, }, } if err := app.Run(os.Args); err != nil { - log.Println(prefix(red.bold(err))) + log.Fatal(err) os.Exit(1) } } -// New return default realize config -func new() realize { - return realize{ - sync: make(chan string), - Settings: Settings{ - file: file, - Legacy: Legacy{ - Interval: 100 * time.Millisecond, - }, - }, - Server: Server{ - parent: &r, - Status: false, - Open: false, - Host: host, - Port: port, - }, - } +// Stop realize workflow +func (r *Realize) Stop() { + close(r.exit) } -// Prefix a given string -func prefix(s string) string { - if s != "" { - return fmt.Sprint(yellow.bold("["), "REALIZE", yellow.bold("]"), " : ", s) +// Run realize workflow +func (r *Realize) Start() { + r.exit = make(chan os.Signal, 2) + signal.Notify(r.exit, os.Interrupt, syscall.SIGTERM) + for k := range r.Schema.Projects { + r.Schema.Projects[k].parent = r + r.Schema.Projects[k].Setup() + go r.Schema.Projects[k].Watch(r.exit) } - return s -} - -// Before is launched before each command -func before(*cli.Context) (err error) { - // custom log - log.SetFlags(0) - log.SetOutput(logWriter{}) - // Before of every exec of a cli method - gopath := build.Default.GOPATH - if gopath == "" { - return errors.New("$GOPATH isn't set properly") - } - if err = os.Setenv("GOPATH", gopath); err != nil { - return err - } - // new realize instance - r = new() - // read if exist - r.Settings.read(&r) - // increase the file limit - if r.Settings.FileLimit != 0 { - if err = r.Settings.flimit(); err != nil { - return err + for { + select { + case <-r.exit: + return } } - return +} + +// Prefix a given string with tool name +func (r *Realize) Prefix(input string) string { + if len(input) > 0 { + return fmt.Sprint(yellow.bold("["), strings.ToUpper(RPrefix), yellow.bold("]"), " : ", input) + } + return input } // Rewrite the layout of the log timestamp -func (w logWriter) Write(bytes []byte) (int, error) { +func (w LogWriter) Write(bytes []byte) (int, error) { return fmt.Fprint(output, yellow.regular("["), time.Now().Format("15:04:05"), yellow.regular("]"), string(bytes)) } diff --git a/realize_test.go b/realize_test.go deleted file mode 100644 index f22439b..0000000 --- a/realize_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "fmt" - "gopkg.in/urfave/cli.v2" - "reflect" - "testing" -) - -func TestPrefix(t *testing.T) { - input := random(10) - value := fmt.Sprint(yellow.bold("["), "REALIZE", yellow.bold("]"), " : ", input) - result := prefix(input) - if result == "" { - t.Fatal("Expected a string") - } - if result != value { - t.Fatal("Expected", value, "Instead", result) - } -} - -func TestBefore(t *testing.T) { - context := cli.Context{} - if err := before(&context); err != nil { - t.Fatal(err) - } -} - -func TestNew(t *testing.T) { - r := new() - if reflect.TypeOf(r).String() != "main.realize" { - t.Error("Expected a realize struct") - } -} diff --git a/schema.go b/schema.go new file mode 100644 index 0000000..338298b --- /dev/null +++ b/schema.go @@ -0,0 +1,76 @@ +package main + +import ( + "errors" + "gopkg.in/urfave/cli.v2" + "path/filepath" + "reflect" +) + +type Schema struct { + Projects []Project `yaml:"projects" json:"projects"` +} + +// Add a project if unique +func (s *Schema) Add(p Project) { + for _, val := range s.Projects { + if reflect.DeepEqual(val, p) { + return + } + } + s.Projects = append(s.Projects, p) +} + +// Remove a project +func (s *Schema) Remove(name string) error { + for key, val := range s.Projects { + if name == val.Name { + s.Projects = append(s.Projects[:key], s.Projects[key+1:]...) + return nil + } + } + return errors.New("project not found") +} + +// New create a project using cli fields +func (s *Schema) New(c *cli.Context) Project { + name := filepath.Base(c.String("path")) + if name == "." { + name = filepath.Base(wdir()) + } + project := Project{ + Name: name, + Path: c.String("path"), + Tools: Tools{ + Vet: Tool{ + Status: c.Bool("vet"), + }, + Fmt: Tool{ + Status: c.Bool("fmt"), + }, + Test: Tool{ + Status: c.Bool("test"), + }, + Generate: Tool{ + Status: c.Bool("generate"), + }, + Build: Tool{ + Status: c.Bool("build"), + }, + Install: Tool{ + Status: c.Bool("install"), + }, + Run: c.Bool("run"), + }, + Args: params(c), + Watcher: Watch{ + Paths: []string{"/"}, + Ignore: []string{".git", ".realize", "vendor"}, + Exts: []string{"go"}, + }, + } + return project +} + +// Filter project list by field +func (s *Schema) Filter(field string, value interface{}) {} diff --git a/server.go b/server.go index 98fd7ed..386b935 100644 --- a/server.go +++ b/server.go @@ -2,12 +2,9 @@ package main import ( "bytes" - "encoding/json" "fmt" "github.com/labstack/echo" "github.com/labstack/echo/middleware" - "golang.org/x/net/websocket" - "gopkg.in/urfave/cli.v2" "io" "net/http" "os/exec" @@ -17,13 +14,13 @@ import ( // Dafault host and port const ( - host = "localhost" - port = 5001 + Host = "localhost" + Port = 5001 ) // Server settings type Server struct { - parent *realize + parent *Realize Status bool `yaml:"status" json:"status"` Open bool `yaml:"open" json:"open"` Port int `yaml:"port" json:"port"` @@ -32,138 +29,40 @@ type Server struct { // Websocket projects func (s *Server) projects(c echo.Context) (err error) { - websocket.Handler(func(ws *websocket.Conn) { - msg, _ := json.Marshal(s.parent) - err = websocket.Message.Send(ws, string(msg)) - go func() { - for { - select { - case <-s.parent.sync: - msg, _ := json.Marshal(s.parent) - err = websocket.Message.Send(ws, string(msg)) - if err != nil { - break - } - } - } - }() - for { - // Read - text := "" - err = websocket.Message.Receive(ws, &text) - if err != nil { - break - } else { - err := json.Unmarshal([]byte(text), &s.parent) - if err == nil { - s.parent.Settings.record(s.parent) - break - } - } - } - ws.Close() - }).ServeHTTP(c.Response(), c.Request()) + //websocket.Handler(func(ws *websocket.Conn) { + // msg, _ := json.Marshal(s.parent) + // err = websocket.Message.Send(ws, string(msg)) + // go func() { + // for { + // select { + // case <-s.parent.sync: + // msg, _ := json.Marshal(s.parent) + // err = websocket.Message.Send(ws, string(msg)) + // if err != nil { + // break + // } + // } + // } + // }() + // for { + // // Read + // text := "" + // err = websocket.Message.Receive(ws, &text) + // if err != nil { + // break + // } else { + // err := json.Unmarshal([]byte(text), &s.parent) + // if err == nil { + // s.parent.Settings.record(s.parent) + // break + // } + // } + // } + // ws.Close() + //}).ServeHTTP(c.Response(), c.Request()) return nil } -// Start the web server -func (s *Server) start(p *cli.Context) (err error) { - if p.Bool("server") { - s.parent.Server.Status = true - } - if p.Bool("open") { - s.parent.Server.Open = true - } - - if s.parent.Server.Status { - e := echo.New() - e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Level: 2, - })) - e.Use(middleware.Recover()) - - // web panel - e.GET("/", func(c echo.Context) error { - return s.render(c, "assets/index.html", 1) - }) - e.GET("/assets/js/all.min.js", func(c echo.Context) error { - return s.render(c, "assets/assets/js/all.min.js", 2) - }) - e.GET("/assets/css/app.css", func(c echo.Context) error { - return s.render(c, "assets/assets/css/app.css", 3) - }) - e.GET("/app/components/settings/index.html", func(c echo.Context) error { - return s.render(c, "assets/app/components/settings/index.html", 1) - }) - e.GET("/app/components/project/index.html", func(c echo.Context) error { - return s.render(c, "assets/app/components/project/index.html", 1) - }) - e.GET("/app/components/index.html", func(c echo.Context) error { - return s.render(c, "assets/app/components/index.html", 1) - }) - e.GET("/assets/img/svg/settings.svg", func(c echo.Context) error { - return s.render(c, "assets/assets/img/svg/settings.svg", 4) - }) - e.GET("/assets/img/svg/fullscreen.svg", func(c echo.Context) error { - return s.render(c, "assets/assets/img/svg/fullscreen.svg", 4) - }) - e.GET("/assets/img/svg/add.svg", func(c echo.Context) error { - return s.render(c, "assets/assets/img/svg/add.svg", 4) - }) - e.GET("/assets/img/svg/backspace.svg", func(c echo.Context) error { - return s.render(c, "assets/assets/img/svg/backspace.svg", 4) - }) - e.GET("/assets/img/svg/error.svg", func(c echo.Context) error { - return s.render(c, "assets/assets/img/svg/error.svg", 4) - }) - e.GET("/assets/img/svg/remove.svg", func(c echo.Context) error { - return s.render(c, "assets/assets/img/remove.svg", 4) - }) - e.GET("/assets/img/svg/logo.svg", func(c echo.Context) error { - return s.render(c, "assets/assets/img/svg/logo.svg", 4) - }) - e.GET("/assets/img/fav.png", func(c echo.Context) error { - return s.render(c, "assets/assets/img/fav.png", 5) - }) - e.GET("/assets/img/svg/circle.svg", func(c echo.Context) error { - return s.render(c, "assets/assets/img/svg/circle.svg", 4) - }) - - //websocket - e.GET("/ws", s.projects) - e.HideBanner = true - e.Debug = false - go e.Start(string(s.parent.Server.Host) + ":" + strconv.Itoa(s.parent.Server.Port)) - _, err = s.openURL("http://" + string(s.parent.Server.Host) + ":" + strconv.Itoa(s.parent.Server.Port)) - if err != nil { - return err - } - } - return nil -} - -// OpenURL in a new tab of default browser -func (s *Server) openURL(url string) (io.Writer, error) { - stderr := bytes.Buffer{} - cmd := map[string]string{ - "windows": "start", - "darwin": "open", - "linux": "xdg-open", - } - if s.Open { - open, err := cmd[runtime.GOOS] - if !err { - return nil, fmt.Errorf("operating system %q is not supported", runtime.GOOS) - } - cmd := exec.Command(open, url) - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return cmd.Stderr, err - } - } - return nil, nil -} - // Render return a web pages defined in bindata func (s *Server) render(c echo.Context, path string, mime int) error { data, err := Asset(path) @@ -193,3 +92,89 @@ func (s *Server) render(c echo.Context, path string, mime int) error { rs.Write(data) return nil } + +// Start the web server +func (s *Server) Start() (err error) { + e := echo.New() + e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 2, + })) + e.Use(middleware.Recover()) + + // web panel + e.GET("/", func(c echo.Context) error { + return s.render(c, "assets/index.html", 1) + }) + e.GET("/assets/js/all.min.js", func(c echo.Context) error { + return s.render(c, "assets/assets/js/all.min.js", 2) + }) + e.GET("/assets/css/app.css", func(c echo.Context) error { + return s.render(c, "assets/assets/css/app.css", 3) + }) + e.GET("/app/components/settings/index.html", func(c echo.Context) error { + return s.render(c, "assets/app/components/settings/index.html", 1) + }) + e.GET("/app/components/project/index.html", func(c echo.Context) error { + return s.render(c, "assets/app/components/project/index.html", 1) + }) + e.GET("/app/components/index.html", func(c echo.Context) error { + return s.render(c, "assets/app/components/index.html", 1) + }) + e.GET("/assets/img/svg/settings.svg", func(c echo.Context) error { + return s.render(c, "assets/assets/img/svg/settings.svg", 4) + }) + e.GET("/assets/img/svg/fullscreen.svg", func(c echo.Context) error { + return s.render(c, "assets/assets/img/svg/fullscreen.svg", 4) + }) + e.GET("/assets/img/svg/add.svg", func(c echo.Context) error { + return s.render(c, "assets/assets/img/svg/add.svg", 4) + }) + e.GET("/assets/img/svg/backspace.svg", func(c echo.Context) error { + return s.render(c, "assets/assets/img/svg/backspace.svg", 4) + }) + e.GET("/assets/img/svg/error.svg", func(c echo.Context) error { + return s.render(c, "assets/assets/img/svg/error.svg", 4) + }) + e.GET("/assets/img/svg/remove.svg", func(c echo.Context) error { + return s.render(c, "assets/assets/img/remove.svg", 4) + }) + e.GET("/assets/img/svg/logo.svg", func(c echo.Context) error { + return s.render(c, "assets/assets/img/svg/logo.svg", 4) + }) + e.GET("/assets/img/fav.png", func(c echo.Context) error { + return s.render(c, "assets/assets/img/fav.png", 5) + }) + e.GET("/assets/img/svg/circle.svg", func(c echo.Context) error { + return s.render(c, "assets/assets/img/svg/circle.svg", 4) + }) + + //websocket + e.GET("/ws", s.projects) + e.HideBanner = true + e.Debug = false + go e.Start(string(s.parent.Server.Host) + ":" + strconv.Itoa(s.parent.Server.Port)) + return nil +} + +// OpenURL in a new tab of default browser +func (s *Server) OpenURL() (io.Writer, error) { + url := "http://" + string(s.parent.Server.Host) + ":" + strconv.Itoa(s.parent.Server.Port) + stderr := bytes.Buffer{} + cmd := map[string]string{ + "windows": "start", + "darwin": "open", + "linux": "xdg-open", + } + if s.Open { + open, err := cmd[runtime.GOOS] + if !err { + return nil, fmt.Errorf("operating system %q is not supported", runtime.GOOS) + } + cmd := exec.Command(open, url) + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return cmd.Stderr, err + } + } + return nil, nil +} diff --git a/server_test.go b/server_test.go deleted file mode 100644 index 3ff9780..0000000 --- a/server_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -//import ( -// "fmt" -// "net/http" -// "testing" -//) -// -//func TestServer_Start(t *testing.T) { -// s := Server{ -// Status: true, -// Open: false, -// Host: "localhost", -// Port: 5000, -// } -// err := s.start(nil) -// if err != nil { -// t.Fatal(err) -// } -// host := "http://localhost:5000/" -// urls := []string{ -// host, -// host + "assets/js/all.min.js", -// host + "assets/css/app.css", -// host + "app/components/settings/index.html", -// host + "app/components/project/index.html", -// host + "app/components/project/index.html", -// host + "app/components/index.html", -// host + "assets/img/svg/ic_settings_black_24px.svg", -// host + "assets/img/svg/ic_fullscreen_black_24px.svg", -// host + "assets/img/svg/ic_add_black_24px.svg", -// host + "assets/img/svg/ic_keyboard_backspace_black_24px.svg", -// host + "assets/img/svg/ic_error_black_48px.svg", -// host + "assets/img/svg/ic_remove_black_24px.svg", -// host + "assets/img/svg/logo.svg", -// host + "assets/img/favicon-32x32.png", -// host + "assets/img/svg/ic_swap_vertical_circle_black_48px.svg", -// } -// for _, elm := range urls { -// resp, err := http.Get(elm) -// if err != nil || resp.StatusCode != 200 { -// t.Fatal(err, resp.StatusCode, elm) -// } -// } -//} -// -//func TestServer_Open(t *testing.T) { -// c := Server{ -// Open: true, -// } -// url := "open_test" -// out, err := c.openURL(url) -// if err == nil { -// t.Fatal("Unexpected, invalid url", url, err) -// } -// output := fmt.Sprint(out) -// if output == "" { -// t.Fatal("Unexpected, invalid url", url, output) -// } -//} diff --git a/settings.go b/settings.go index e941874..e553500 100644 --- a/settings.go +++ b/settings.go @@ -12,12 +12,11 @@ import ( // settings const const ( - permission = 0775 - directory = ".realize" - file = "realize.yaml" - fileOut = "outputs.log" - fileErr = "errors.log" - fileLog = "logs.log" + Permission = 0775 + File = "realize.yaml" + FileOut = "outputs.log" + FileErr = "errors.log" + FileLog = "logs.log" ) // random string preference @@ -30,7 +29,6 @@ const ( // Settings defines a group of general settings and options type Settings struct { - file string Files `yaml:"files,omitempty" json:"files,omitempty"` Legacy Legacy `yaml:"legacy" json:"legacy"` FileLimit int32 `yaml:"flimit,omitempty" json:"flimit,omitempty"` @@ -75,8 +73,8 @@ func random(n int) string { return string(b) } -// Delete realize folder -func (s *Settings) del(d string) error { +// Remove realize folder +func (s *Settings) Remove(d string) error { _, err := os.Stat(d) if !os.IsNotExist(err) { return os.RemoveAll(d) @@ -84,23 +82,17 @@ func (s *Settings) del(d string) error { return err } -// Validate checks a fatal error -func (s Settings) validate(err error) error { - if err != nil { - s.fatal(err, "") - } - return nil -} - -// Read from config file -func (s *Settings) read(out interface{}) error { - localConfigPath := s.file +// Read config file +func (s *Settings) Read(out interface{}) error { + localConfigPath := RFile // backward compatibility - path := filepath.Join(directory, s.file) + path := filepath.Join(RDir, RFile) if _, err := os.Stat(path); err == nil { localConfigPath = path + } else { + return nil } - content, err := s.stream(localConfigPath) + content, err := s.Stream(localConfigPath) if err == nil { err = yaml.Unmarshal(content, out) return err @@ -108,55 +100,52 @@ func (s *Settings) read(out interface{}) error { return err } -// Record create and unmarshal the yaml config file -func (s *Settings) record(out interface{}) error { +// Write config file +func (s *Settings) Write(out interface{}) error { y, err := yaml.Marshal(out) if err != nil { return err } - if _, err := os.Stat(directory); os.IsNotExist(err) { - if err = os.Mkdir(directory, permission); err != nil { - return s.write(s.file, y) + if _, err := os.Stat(RDir); os.IsNotExist(err) { + if err = os.Mkdir(RDir, Permission); err != nil { + s.Fatal(ioutil.WriteFile(RFile, y, Permission)) } } - return s.write(filepath.Join(directory, s.file), y) + s.Fatal(ioutil.WriteFile(filepath.Join(RDir, RFile), y, Permission)) + return nil } // Stream return a byte stream of a given file -func (s Settings) stream(file string) ([]byte, error) { +func (s Settings) Stream(file string) ([]byte, error) { _, err := os.Stat(file) if err != nil { return nil, err } content, err := ioutil.ReadFile(file) - s.validate(err) + s.Fatal(err) return content, err } // Fatal prints a fatal error with its additional messages -func (s Settings) fatal(err error, msg ...interface{}) { - if len(msg) > 0 && err != nil { - log.Fatalln(red.regular(msg...), err.Error()) - } else if err != nil { - log.Fatalln(err.Error()) +func (s Settings) Fatal(err error, msg ...interface{}) { + if err != nil { + if len(msg) > 0 { + log.Fatalln(red.regular(msg...), err.Error()) + } else { + log.Fatalln(err.Error()) + } } } -// Write a file -func (s Settings) write(name string, data []byte) error { - err := ioutil.WriteFile(name, data, permission) - return s.validate(err) -} - // Create a new file and return its pointer -func (s Settings) create(path string, name string) *os.File { +func (s Settings) Create(path string, name string) *os.File { var file string - if _, err := os.Stat(directory); err == nil { - file = filepath.Join(path, directory, name) + if _, err := os.Stat(RDir); err == nil { + file = filepath.Join(path, RDir, name) } else { file = filepath.Join(path, name) } - out, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE|os.O_SYNC, permission) - s.validate(err) + out, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE|os.O_SYNC, Permission) + s.Fatal(err) return out } diff --git a/settings_test.go b/settings_test.go deleted file mode 100644 index f084abf..0000000 --- a/settings_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "errors" - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -func TestSettings_Flimit(t *testing.T) { - s := Settings{} - s.FileLimit = 100 - if err := s.flimit(); err != nil { - t.Fatal("Unable to increase limit", err) - } -} - -func TestSettings_Stream(t *testing.T) { - s := Settings{} - filename := random(4) - if _, err := s.stream(filename); err == nil { - t.Fatal("Error expected, none found", filename, err) - } - - filename = "settings.go" - if _, err := s.stream(filename); err != nil { - t.Fatal("Error unexpected", filename, err) - } -} - -func TestSettings_Write(t *testing.T) { - s := Settings{} - data := "abcdefgh" - d, err := ioutil.TempFile("", "io_test") - if err != nil { - t.Fatal(err) - } - if err := s.write(d.Name(), []byte(data)); err != nil { - t.Fatal(err) - } -} - -func TestSettings_Create(t *testing.T) { - s := Settings{} - p, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - f := s.create(p, "io_test") - os.Remove(f.Name()) -} - -func TestSettings_Read(t *testing.T) { - s := Settings{} - var a interface{} - s.file = "settings_b" - if err := s.read(a); err == nil { - t.Fatal("Error unexpected", err) - } - - s.file = "settings_test.yaml" - dir, err := ioutil.TempDir("", directory) - if err != nil { - t.Fatal(err) - } - d, err := ioutil.TempFile(dir, "settings_test.yaml") - if err != nil { - t.Fatal(err) - } - s.file = d.Name() - if err := s.read(a); err != nil { - t.Fatal("Error unexpected", err) - } -} - -func TestSettings_Del(t *testing.T) { - s := Settings{} - if err := s.del("abcd"); err == nil { - t.Fatal("Error unexpected, dir dosn't exist", err) - } - - d, err := ioutil.TempDir("", "settings_test") - if err != nil { - t.Fatal(err) - } - if err := s.del(d); err != nil { - t.Fatal("Error unexpected, dir exist", err) - } -} - -func TestSettings_Record(t *testing.T) { - s := Settings{} - s.file = "settings_test.yaml" - var a interface{} - if err := s.record(a); err != nil { - t.Fatal(err) - } - s.del(filepath.Join(directory, s.file)) -} - -func TestSettings_Validate(t *testing.T) { - s := Settings{} - input := errors.New("") - input = nil - if err := s.validate(input); err != nil { - t.Error("Expected", input, "instead", err) - } -} - -func TestSettings_Fatal(t *testing.T) { - s := Settings{} - s.fatal(nil, "test") -} diff --git a/settings_unix.go b/settings_unix.go index 9e72710..1afb3f0 100644 --- a/settings_unix.go +++ b/settings_unix.go @@ -5,7 +5,7 @@ package main import "syscall" // Flimit defines the max number of watched files -func (s *Settings) flimit() error { +func (s *Settings) Flimit() error { var rLimit syscall.Rlimit rLimit.Max = uint64(s.FileLimit) rLimit.Cur = uint64(s.FileLimit) diff --git a/settings_windows.go b/settings_windows.go index 3a11d85..13dc18a 100644 --- a/settings_windows.go +++ b/settings_windows.go @@ -3,6 +3,6 @@ package main // Flimit defines the max number of watched files -func (s *Settings) flimit() error { +func (s *Settings) Flimit() error { return nil } diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..7313a58 --- /dev/null +++ b/tools.go @@ -0,0 +1,160 @@ +package main + +import ( + "bytes" + "errors" + "os/exec" + "path/filepath" + "strings" +) + +type Tool struct { + Args []string `yaml:"args,omitempty" json:"args,omitempty"` + Method string `yaml:"method,omitempty" json:"method,omitempty"` + Status bool `yaml:"status,omitempty" json:"status,omitempty"` + dir bool + isTool bool + method []string + cmd []string + name string +} + +type Tools struct { + Fix Tool `yaml:"fix,omitempty" json:"fix,omitempty"` + Clean Tool `yaml:"clean,omitempty" json:"clean,omitempty"` + Vet Tool `yaml:"vet,omitempty" json:"vet,omitempty"` + Fmt Tool `yaml:"fmt,omitempty" json:"fmt,omitempty"` + Test Tool `yaml:"test,omitempty" json:"test,omitempty"` + Generate Tool `yaml:"generate,omitempty" json:"generate,omitempty"` + Install Tool `yaml:"install,omitempty" json:"install,omitempty"` + Build Tool `yaml:"build,omitempty" json:"build,omitempty"` + Run bool `yaml:"run,omitempty" json:"run,omitempty"` +} + +func (t *Tools) Setup() { + // go clean + if t.Clean.Status { + t.Clean.name = "Clean" + t.Clean.isTool = true + t.Clean.cmd = replace([]string{"go clean"}, t.Clean.Method) + t.Clean.Args = split([]string{}, t.Clean.Args) + } + // go generate + if t.Generate.Status { + t.Generate.dir = true + t.Generate.isTool = true + t.Generate.name = "Generate" + t.Generate.cmd = replace([]string{"go", "generate"}, t.Generate.Method) + t.Generate.Args = split([]string{}, t.Generate.Args) + } + // go fix + if t.Fix.Status { + t.Fix.name = "Fix" + t.Fix.isTool = true + t.Fix.cmd = replace([]string{"go fix"}, t.Fix.Method) + t.Fix.Args = split([]string{}, t.Fix.Args) + } + // go fmt + if t.Fmt.Status { + if len(t.Fmt.Args) == 0 { + t.Fmt.Args = []string{"-s", "-w", "-e", "./"} + } + t.Fmt.name = "Fmt" + t.Fmt.isTool = true + t.Fmt.cmd = replace([]string{"gofmt"}, t.Fmt.Method) + t.Fmt.Args = split([]string{}, t.Fmt.Args) + } + // go vet + if t.Vet.Status { + t.Vet.dir = true + t.Vet.name = "Vet" + t.Vet.isTool = true + t.Vet.cmd = replace([]string{"go", "vet"}, t.Vet.Method) + t.Vet.Args = split([]string{}, t.Vet.Args) + } + // go test + if t.Test.Status { + t.Test.dir = true + t.Test.isTool = true + t.Test.name = "Test" + t.Test.cmd = replace([]string{"go", "test"}, t.Test.Method) + t.Test.Args = split([]string{}, t.Test.Args) + } + // go install + t.Install.name = "Install" + t.Install.cmd = replace([]string{"go", "install"}, t.Install.Method) + t.Install.Args = split([]string{}, t.Install.Args) + // go build + if t.Build.Status { + t.Build.name = "Build" + t.Build.cmd = replace([]string{"go", "build"}, t.Build.Method) + t.Build.Args = split([]string{}, t.Build.Args) + } +} + +func (t *Tool) Exec(path string, stop <-chan bool) (response Response) { + if t.dir && filepath.Ext(path) != "" { + path = filepath.Dir(path) + } + if strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "") { + args := []string{} + if strings.HasSuffix(path, ".go") { + args = append(t.Args, path) + path = filepath.Dir(path) + } + if s := ext(path); s == "" || s == "go" { + var out, stderr bytes.Buffer + done := make(chan error) + args = append(t.cmd, t.Args...) + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = path + cmd.Stdout = &out + cmd.Stderr = &stderr + // Start command + cmd.Start() + go func() { done <- cmd.Wait() }() + // Wait a result + select { + case <-stop: + // Stop running command + cmd.Process.Kill() + case err := <-done: + // Command completed + response.Name = t.name + if err != nil { + response.Err = errors.New(stderr.String() + out.String()) + } else { + response.Out = out.String() + } + } + } + } + return +} + +func (t *Tool) Compile(path string, stop <-chan bool) (response Response) { + var out bytes.Buffer + var stderr bytes.Buffer + done := make(chan error) + args := append(t.cmd, t.Args...) + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = filepath.Dir(path) + cmd.Stdout = &out + cmd.Stderr = &stderr + // Start command + cmd.Start() + go func() { done <- cmd.Wait() }() + // Wait a result + select { + case <-stop: + // Stop running command + cmd.Process.Kill() + case err := <-done: + // Command completed + response.Name = t.name + if err != nil { + response.Err = errors.New(stderr.String() + err.Error()) + } + } + return +} diff --git a/utils.go b/utils.go index 94fa86e..27419ae 100644 --- a/utils.go +++ b/utils.go @@ -76,7 +76,7 @@ func replace(a []string, b string) []string { func wdir() string { dir, err := os.Getwd() if err != nil { - log.Fatal(prefix(err.Error())) + log.Fatal(err.Error()) } return dir } diff --git a/watcher.go b/watcher.go deleted file mode 100644 index 0e6d5e2..0000000 --- a/watcher.go +++ /dev/null @@ -1,532 +0,0 @@ -package main - -import ( - "fmt" - "github.com/fsnotify/fsnotify" - "log" - "math/big" - "os" - "os/signal" - "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "time" -) - -var ( - msg string - out BufferOut - wg sync.WaitGroup -) - -const ( - msgStop = "killed" - extWindows = ".exe" -) - -// Watch struct defines options for livereload -type Watch struct { - Preview bool `yaml:"preview,omitempty" json:"preview,omitempty"` - Paths []string `yaml:"paths" json:"paths"` - Exts []string `yaml:"extensions" json:"extensions"` - Ignore []string `yaml:"ignored_paths,omitempty" json:"ignored_paths,omitempty"` - Scripts []Command `yaml:"scripts,omitempty" json:"scripts,omitempty"` -} - -// Result channel with data stream and errors -type Result struct { - stream string - err error -} - -// Buffer define an array buffer for each log files -type Buffer struct { - StdOut []BufferOut `json:"stdOut"` - StdLog []BufferOut `json:"stdLog"` - StdErr []BufferOut `json:"stdErr"` -} - -// Project defines the informations of a single project -type Project struct { - parent *realize - watcher FileWatcher - init bool - Settings `yaml:"-" json:"-"` - files, folders int64 - name, lastFile string - tools []tool - paths []string - lastTime time.Time - Name string `yaml:"name" json:"name"` - Path string `yaml:"path" json:"path"` - Environment map[string]string `yaml:"environment,omitempty" json:"environment,omitempty"` - Cmds Cmds `yaml:"commands" json:"commands"` - Args []string `yaml:"args,omitempty" json:"args,omitempty"` - Watcher Watch `yaml:"watcher" json:"watcher"` - Buffer Buffer `yaml:"-" json:"buffer"` - ErrorOutputPattern string `yaml:"errorOutputPattern,omitempty" json:"errorOutputPattern,omitempty"` -} - -// Command options -type Command struct { - Type string `yaml:"type" json:"type"` - Command string `yaml:"command" json:"command"` - Path string `yaml:"path,omitempty" json:"path,omitempty"` - Global bool `yaml:"global,omitempty" json:"global,omitempty"` - Output bool `yaml:"output,omitempty" json:"output,omitempty"` -} - -// BufferOut is used for exchange information between "realize cli" and "web realize" -type BufferOut struct { - Time time.Time `json:"time"` - Text string `json:"text"` - Path string `json:"path"` - Type string `json:"type"` - Stream string `json:"stream"` - Errors []string `json:"errors"` -} - -// Watch the project -func (p *Project) watch() { - p.watcher, _ = Watcher(r.Settings.Legacy.Force, r.Settings.Legacy.Interval) - stop, exit := make(chan bool), make(chan os.Signal, 2) - signal.Notify(exit, os.Interrupt, syscall.SIGTERM) - // before global commands - p.cmd(stop, "before", true) - // indexing files and dirs - for _, dir := range p.Watcher.Paths { - base, _ := filepath.Abs(p.Path) - base = filepath.Join(base, dir) - if _, err := os.Stat(base); err == nil { - if err := filepath.Walk(base, p.walk); err == nil { - p.tool(stop, base) - } - } else { - p.err(err) - } - } - // indexing done, files and folders - msg = fmt.Sprintln(p.pname(p.Name, 1), ":", blue.bold("Watching"), magenta.bold(p.files), "file/s", magenta.bold(p.folders), "folder/s") - out = BufferOut{Time: time.Now(), Text: "Watching " + strconv.FormatInt(p.files, 10) + " files/s " + strconv.FormatInt(p.folders, 10) + " folder/s"} - p.stamp("log", out, msg, "") - // start - go p.routines(stop, p.watcher, "") - //is watching -L: - for { - select { - case event := <-p.watcher.Events(): - if time.Now().Truncate(time.Second).After(p.lastTime) || event.Name != p.lastFile { - // event time - eventTime := time.Now() - // file extension - ext := ext(event.Name) - if ext == "" { - ext = "DIR" - } - // change message - msg = fmt.Sprintln(p.pname(p.Name, 4), ":", magenta.bold(strings.ToUpper(ext)), "changed", magenta.bold(event.Name)) - out = BufferOut{Time: time.Now(), Text: ext + " changed " + event.Name} - // switch event type - switch event.Op { - case fsnotify.Chmod: - case fsnotify.Remove: - p.watcher.Remove(event.Name) - if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) { - close(stop) - stop = make(chan bool) - p.stamp("log", out, msg, "") - go p.routines(stop, p.watcher, "") - } - default: - file, err := os.Stat(event.Name) - if err != nil { - continue - } - if file.IsDir() { - filepath.Walk(event.Name, p.walk) - } else if file.Size() > 0 { - // used only for test and debug - if p.parent.Settings.Recovery { - log.Println(event) - } - if !strings.Contains(ext, "_") && !strings.Contains(ext, ".") && array(ext, p.Watcher.Exts) { - // change watched - // check if a file is still writing #119 - if event.Op != fsnotify.Write || (eventTime.Truncate(time.Millisecond).After(file.ModTime().Truncate(time.Millisecond)) || event.Name != p.lastFile) { - close(stop) - stop = make(chan bool) - // stop and start again - p.stamp("log", out, msg, "") - go p.routines(stop, p.watcher, event.Name) - } - } - p.lastTime = time.Now().Truncate(time.Second) - p.lastFile = event.Name - } - } - } - case err := <-p.watcher.Errors(): - p.err(err) - case <-exit: - p.cmd(nil, "after", true) - break L - } - } - wg.Done() - return -} - -// Error occurred -func (p *Project) err(err error) { - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.regular(err.Error())) - out = BufferOut{Time: time.Now(), Text: err.Error()} - p.stamp("error", out, msg, "") -} - -// Config project init -func (p *Project) config(r *realize) { - // get basepath name - p.name = filepath.Base(p.Path) - // env variables - for key, item := range p.Environment { - if err := os.Setenv(key, item); err != nil { - p.Buffer.StdErr = append(p.Buffer.StdErr, BufferOut{Time: time.Now(), Text: err.Error(), Type: "Env error", Stream: ""}) - } - } - // init commands - if len(p.Cmds.Fmt.Args) == 0 { - p.Cmds.Fmt.Args = []string{"-s", "-w", "-e", "./"} - } - p.tools = append(p.tools, tool{ - status: p.Cmds.Clean.Status, - cmd: replace([]string{"go clean"}, p.Cmds.Clean.Method), - options: split([]string{}, p.Cmds.Clean.Args), - name: "Clean", - }) - p.tools = append(p.tools, tool{ - status: p.Cmds.Generate.Status, - cmd: replace([]string{"go", "generate"}, p.Cmds.Generate.Method), - options: split([]string{}, p.Cmds.Generate.Args), - name: "Generate", - dir: true, - }) - p.tools = append(p.tools, tool{ - status: p.Cmds.Fix.Status, - cmd: replace([]string{"go fix"}, p.Cmds.Fix.Method), - options: split([]string{}, p.Cmds.Fix.Args), - name: "Fix", - }) - p.tools = append(p.tools, tool{ - status: p.Cmds.Fmt.Status, - cmd: replace([]string{"gofmt"}, p.Cmds.Fmt.Method), - options: split([]string{}, p.Cmds.Fmt.Args), - name: "Fmt", - }) - p.tools = append(p.tools, tool{ - status: p.Cmds.Vet.Status, - cmd: replace([]string{"go", "vet"}, p.Cmds.Vet.Method), - options: split([]string{}, p.Cmds.Vet.Args), - name: "Vet", - dir: true, - }) - p.tools = append(p.tools, tool{ - status: p.Cmds.Test.Status, - cmd: replace([]string{"go", "test"}, p.Cmds.Test.Method), - options: split([]string{}, p.Cmds.Test.Args), - name: "Test", - dir: true, - }) - p.Cmds.Install = Cmd{ - Status: p.Cmds.Install.Status, - Args: append([]string{}, p.Cmds.Install.Args...), - method: replace([]string{"go", "install"}, p.Cmds.Install.Method), - name: "Install", - startTxt: "Installing...", - endTxt: "Installed", - } - p.Cmds.Build = Cmd{ - Status: p.Cmds.Build.Status, - Args: append([]string{}, p.Cmds.Build.Args...), - method: replace([]string{"go", "build"}, p.Cmds.Build.Method), - name: "Build", - startTxt: "Building...", - endTxt: "Built", - } - p.parent = r -} - -// Cmd calls the method that execute commands after/before and display the results -func (p *Project) cmd(stop <-chan bool, flag string, global bool) { - done := make(chan bool) - // cmds are scheduled in sequence - go func() { - for _, cmd := range p.Watcher.Scripts { - if strings.ToLower(cmd.Type) == flag && cmd.Global == global { - err, logs := p.command(stop, cmd) - if err == "" && logs == "" { - continue - } - msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.bold("Command"), green.bold("\"")+cmd.Command+green.bold("\"")) - out = BufferOut{Time: time.Now(), Text: cmd.Command, Type: flag} - if err != "" { - p.stamp("error", out, msg, "") - - msg = fmt.Sprintln(red.regular(err)) - out = BufferOut{Time: time.Now(), Text: err, Type: flag} - p.stamp("error", out, "", msg) - - } else if logs != "" && cmd.Output { - msg = fmt.Sprintln(logs) - out = BufferOut{Time: time.Now(), Text: logs, Type: flag} - p.stamp("log", out, "", msg) - } else { - p.stamp("log", out, msg, "") - } - } - } - close(done) - }() - for { - select { - case <-stop: - return - case <-done: - return - } - } -} - -// Compile is used for run and display the result of a compiling -func (p *Project) compile(stop <-chan bool, cmd Cmd) error { - if cmd.Status { - var start time.Time - channel := make(chan Result) - go func() { - log.Println(p.pname(p.Name, 1), ":", cmd.startTxt) - start = time.Now() - stream, err := p.goCompile(stop, cmd.method, cmd.Args) - if stream != msgStop { - channel <- Result{stream, err} - } - }() - select { - case r := <-channel: - if r.err != nil { - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(cmd.name), red.regular(r.err.Error())) - out = BufferOut{Time: time.Now(), Text: r.err.Error(), Type: cmd.name, Stream: r.stream} - p.stamp("error", out, msg, r.stream) - } else { - msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.regular(cmd.endTxt), "in", magenta.regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) - out = BufferOut{Time: time.Now(), Text: cmd.name + " in " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} - p.stamp("log", out, msg, r.stream) - } - return r.err - case <-stop: - return nil - } - } - return nil -} - -// Defines the colors scheme for the project name -func (p *Project) pname(name string, color int) string { - switch color { - case 1: - name = yellow.regular("[") + strings.ToUpper(name) + yellow.regular("]") - break - case 2: - name = yellow.regular("[") + red.bold(strings.ToUpper(name)) + yellow.regular("]") - break - case 3: - name = yellow.regular("[") + blue.bold(strings.ToUpper(name)) + yellow.regular("]") - break - case 4: - name = yellow.regular("[") + magenta.bold(strings.ToUpper(name)) + yellow.regular("]") - break - case 5: - name = yellow.regular("[") + green.bold(strings.ToUpper(name)) + yellow.regular("]") - break - } - return name -} - -// Tool logs the result of a go command -func (p *Project) tool(stop <-chan bool, path string) error { - if len(path) > 0 { - done := make(chan bool) - result := make(chan tool) - go func() { - var wg sync.WaitGroup - for _, element := range p.tools { - // no need a sequence, these commands can be asynchronous - if element.status { - wg.Add(1) - p.goTool(&wg, stop, result, path, element) - } - } - wg.Wait() - close(done) - }() - loop: - for { - select { - case tool := <-result: - if tool.err != "" { - msg = fmt.Sprintln(p.pname(p.Name, 2), ":", red.bold(tool.name), red.regular("there are some errors in"), ":", magenta.bold(path)) - buff := BufferOut{Time: time.Now(), Text: "there are some errors in", Path: path, Type: tool.name, Stream: tool.err} - p.stamp("error", buff, msg, tool.err) - } else if tool.out != "" { - msg = fmt.Sprintln(p.pname(p.Name, 3), ":", red.bold(tool.name), red.regular("outputs"), ":", blue.bold(path)) - buff := BufferOut{Time: time.Now(), Text: "outputs", Path: path, Type: tool.name, Stream: tool.out} - p.stamp("out", buff, msg, tool.out) - } - case <-done: - break loop - case <-stop: - break loop - } - } - } - return nil -} - -// Watch the files tree of a project -func (p *Project) walk(path string, info os.FileInfo, err error) error { - for _, v := range p.Watcher.Ignore { - s := append([]string{p.Path}, strings.Split(v, string(os.PathSeparator))...) - if strings.Contains(path, filepath.Join(s...)) { - return nil - } - } - if !strings.HasPrefix(path, ".") && (info.IsDir() || array(ext(path), p.Watcher.Exts)) { - result := p.watcher.Walk(path, p.init) - if result != "" { - if info.IsDir() { - p.folders++ - } else { - p.files++ - } - if p.Watcher.Preview { - log.Println(p.pname(p.Name, 1), ":", path) - } - } - } - return nil -} - -// Print on files, cli, ws -func (p *Project) stamp(t string, o BufferOut, msg string, stream string) { - switch t { - case "out": - p.Buffer.StdOut = append(p.Buffer.StdOut, o) - if p.Files.Outputs.Status { - f := p.create(p.Path, p.Files.Outputs.Name) - t := time.Now() - s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n"} - if _, err := f.WriteString(strings.Join(s, " ")); err != nil { - p.fatal(err, "") - } - } - case "log": - p.Buffer.StdLog = append(p.Buffer.StdLog, o) - if p.Files.Logs.Status { - f := p.create(p.Path, p.Files.Logs.Name) - t := time.Now() - s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n"} - if stream != "" { - s = []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Text, "\r\n", stream} - } - if _, err := f.WriteString(strings.Join(s, " ")); err != nil { - p.fatal(err, "") - } - } - case "error": - p.Buffer.StdErr = append(p.Buffer.StdErr, o) - if p.Files.Errors.Status { - f := p.create(p.Path, p.Files.Errors.Name) - t := time.Now() - s := []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Type, o.Text, o.Path, "\r\n"} - if stream != "" { - s = []string{t.Format("2006-01-02 15:04:05"), strings.ToUpper(p.Name), ":", o.Type, o.Text, o.Path, "\r\n", stream} - } - if _, err := f.WriteString(strings.Join(s, " ")); err != nil { - p.fatal(err, "") - } - } - } - if msg != "" { - log.Print(msg) - } - if stream != "" { - fmt.Fprint(output, stream) - } - go func() { - p.parent.sync <- "sync" - }() -} - -// Routines launches the toolchain run, build, install -func (p *Project) routines(stop <-chan bool, watcher FileWatcher, path string) { - var done bool - var install, build error - go func() { - for { - select { - case <-stop: - done = true - return - } - } - }() - if done { - return - } - // before command - p.cmd(stop, "before", false) - if done { - return - } - // Go supported tools - p.tool(stop, path) - // Prevent fake events on polling startup - p.init = true - // prevent errors using realize without config with only run flag - if p.Cmds.Run && !p.Cmds.Install.Status && !p.Cmds.Build.Status { - p.Cmds.Install.Status = true - } - if done { - return - } - install = p.compile(stop, p.Cmds.Install) - if done { - return - } - build = p.compile(stop, p.Cmds.Build) - if done { - return - } - if install == nil && build == nil && p.Cmds.Run { - var start time.Time - runner := make(chan bool, 1) - go func() { - log.Println(p.pname(p.Name, 1), ":", "Running..") - start = time.Now() - p.goRun(stop, runner) - }() - select { - case <-runner: - msg = fmt.Sprintln(p.pname(p.Name, 5), ":", green.regular("Started"), "in", magenta.regular(big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3), " s")) - out = BufferOut{Time: time.Now(), Text: "Started in " + big.NewFloat(float64(time.Since(start).Seconds())).Text('f', 3) + " s"} - p.stamp("log", out, msg, "") - case <-stop: - return - } - } - if done { - return - } - p.cmd(stop, "after", false) -} diff --git a/watcher_test.go b/watcher_test.go deleted file mode 100644 index 223711f..0000000 --- a/watcher_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "testing" - - "os" -) - -type fileWatcherMock struct { - FileWatcher -} - -func (f *fileWatcherMock) Walk(path string, _ bool) string { - return path -} - -type fileInfoMock struct { - os.FileInfo - FileIsDir bool -} - -func (m *fileInfoMock) IsDir() bool { return m.FileIsDir } - -func TestWalk(t *testing.T) { - p := Project{ - Name: "Test Project", - Watcher: Watch{ - Paths: []string{"/"}, - Ignore: []string{"vendor"}, - Exts: []string{"go"}, - }, - Path: "/go/project", - watcher: &fileWatcherMock{}, - init: true, - } - - files := []struct { - Path string - IsDir bool - }{ - // valid files - {"/go/project", true}, - {"/go/project/main.go", false}, - {"/go/project/main_test.go", false}, - // invalid relative path - {"./relative/path", true}, - {"./relative/path/file.go", false}, - // invalid extension - {"/go/project/settings.yaml", false}, - // invalid vendor files - {"/go/project/vendor/foo", true}, - {"/go/project/vendor/foo/main.go", false}, - } - - for _, file := range files { - fileInfo := fileInfoMock{ - FileIsDir: file.IsDir, - } - err := p.walk(file.Path, &fileInfo, nil) - if err != nil { - t.Errorf("Error not expected: %s", err) - } - } - - if p.files != 2 { - t.Errorf("Exepeted %d files, but was %d", 2, p.files) - } - - if p.folders != 1 { - t.Errorf("Exepeted %d folders, but was %d", 2, p.folders) - } -}