├── .github ├── FUNDING.yml ├── CODEOWNERS ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitattributes ├── .gitignore ├── registry.yml ├── utils ├── strings.go ├── cmd_windows.go ├── cmd_unix.go ├── interrupt.go ├── http.go └── ioutil.go ├── CONTRIBUTING.md ├── Dockerfile ├── main.go ├── cmd ├── clean.go ├── unistall.go ├── help.go ├── run.go ├── cmd.go ├── stats_compare.go ├── new.go ├── add.go ├── check.go ├── init.go └── stats.go ├── LICENSE ├── parser ├── modpkg_test.go ├── parser_test.go ├── modpkg.go └── parser.go ├── project ├── registry_test.go ├── registry.go ├── livereload.go └── project.go ├── go.mod ├── CODE_OF_CONDUCT.md ├── snippet └── file.go ├── README.md └── go.sum /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kataras -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | go.sum linguist-generated -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | bin/ 3 | _testfiles/ 4 | .DS_STORE 5 | iris-cli 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | * @kataras 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Other 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Describe the issue you are facing or ask for help 11 | -------------------------------------------------------------------------------- /registry.yml: -------------------------------------------------------------------------------- 1 | Projects: 2 | { 3 | basic: "iris-contrib/basic-template", 4 | mvc: "iris-contrib/mvc-template", 5 | svelte: "iris-contrib/svelte-template", 6 | react-typescript: "iris-contrib/react-typescript-template", 7 | go-admin: "iris-contrib/go-admin-template", 8 | } 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # We'd love to see more contributions 2 | 3 | Read how you can [contribute to the project](https://github.com/kataras/iris-cli/main/CONTRIBUTING.md). 4 | 5 | > Please attach an [issue](https://github.com/kataras/iris-cli/issues) link which your PR solves otherwise your work may be rejected. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Examples for the `iris-cli` project can be found at 2 | . 3 | 4 | Documentation for the `iris-cli` project can be found at 5 | . 6 | 7 | Love the `iris-cli` package? Please consider supporting the project: 8 | 👉 https://paypal.me/kataras 9 | -------------------------------------------------------------------------------- /utils/strings.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // SplitNameVersion accepts a string and returns its name and version. 8 | func SplitNameVersion(s string) (name string, version string) { 9 | nameBranch := strings.Split(s, "@") 10 | name = nameBranch[0] 11 | if len(nameBranch) > 1 { 12 | version = nameBranch[1] 13 | } else { 14 | version = "main" 15 | } 16 | 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | 11 | test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | go_version: [1.23.x] 18 | steps: 19 | 20 | - name: Set up Go 1.x 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: ${{ matrix.go_version }} 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v4 27 | 28 | - name: Test 29 | run: go test -v ./... 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all read our [Code of Conduct](CODE_OF_CONDUCT.md). 4 | 5 | ## PR 6 | 7 | 1. Open a new [issue](https://github.com/kataras/iris-cli/issues/new) 8 | * Write the Operating System and the version of your machine. 9 | * Describe your problem, what did you expect to see and what you see instead. 10 | * If it's a feature request, describe your idea as better as you can 11 | 2. Fork the [repository](https://github.com/kataras/iris-cli). 12 | 3. Make your changes. 13 | 4. Compare & Push the PR from [here](https://github.com/kataras/iris-cli/compare). 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS builder 2 | RUN apt-get update 3 | RUN apt-get install -y git 4 | RUN curl -sL https://deb.nodesource.com/setup_13.x | bash - 5 | RUN apt-get update && apt-get install -y nodejs 6 | ENV GO111MODULE=on \ 7 | CGO_ENABLED=0 \ 8 | GOOS=linux \ 9 | GOARCH=amd64 10 | WORKDIR /build 11 | COPY . . 12 | RUN go build -o /bin/iris-cli . 13 | WORKDIR /bin 14 | RUN chmod +x ./iris-cli 15 | WORKDIR /myproject 16 | # docker image rm -f iris-cli;docker build . -t iris-cli; docker run -i -t -p 8080:8080 -v "C:\Users\kataras\Desktop\myproject:/myproject" iris-cli run svelte 17 | ENTRYPOINT ["iris-cli"] 18 | # FROM scratch 19 | # COPY --from=builder /bin/iris-cli / 20 | # ENTRYPOINT ["/iris-cli"] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: type:bug 6 | assignees: kataras 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. [...] 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. ubuntu, windows] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: type:idea 6 | assignees: kataras 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/kataras/iris-cli/cmd" 8 | ) 9 | 10 | const ( 11 | // buildRevision is the build revision (docker commit string or git rev-parse HEAD) but it's 12 | // available only on the build state, on the cli executable - via the "--version" flag. 13 | buildRevision = "" 14 | // buildTime is the build unix time (in seconds since 1970-01-01 00:00:00 UTC), like the `buildRevision`, 15 | // this is available on after the build state, inside the cli executable - via the "--version" flag. 16 | // 17 | // Note that this buildTime is not int64, it's type of string and it is provided at build time. 18 | // Do not change! 19 | buildTime = "" 20 | ) 21 | 22 | func main() { 23 | app := cmd.New(buildRevision, buildTime) 24 | if err := app.Execute(); err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cmd/clean.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/kataras/iris-cli/project" 7 | "github.com/kataras/iris-cli/utils" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func cleanCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "clean", 15 | Short: "Clean a project after install or build", 16 | SilenceErrors: true, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | name := "." // current directory. 19 | if len(args) > 0 { 20 | name = args[0] 21 | } 22 | 23 | projectPath, err := filepath.Abs(name) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if !utils.Exists(projectPath) { 29 | return project.ErrProjectNotExists 30 | } 31 | 32 | p, err := project.LoadFromDisk(projectPath) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return p.Clean() 38 | }, 39 | } 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /cmd/unistall.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/kataras/iris-cli/project" 7 | "github.com/kataras/iris-cli/utils" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func unistallCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "unistall", 15 | Short: "Removes all project files", 16 | SilenceErrors: true, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | name := "." // current directory. 19 | if len(args) > 0 { 20 | name = args[0] 21 | } 22 | 23 | projectPath, err := filepath.Abs(name) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if !utils.Exists(projectPath) { 29 | return project.ErrProjectNotExists 30 | } 31 | 32 | p, err := project.LoadFromDisk(projectPath) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return p.Unistall() 38 | }, 39 | } 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2024 Gerasimos Maropoulos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cmd/help.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // HelpTemplate is the structure which contaisn the variables for the help command. 12 | type HelpTemplate struct { 13 | BuildTime string 14 | BuildRevision string 15 | ShowGoRuntimeVersion bool 16 | 17 | Template fmt.Stringer 18 | } 19 | 20 | func (h HelpTemplate) String() string { 21 | tmpl := `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} 22 | {{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` 23 | 24 | if h.BuildRevision != "" { 25 | buildTitle := ">>>> build" // if we ever want an emoji, there is one: \U0001f4bb 26 | tab := strings.Repeat(" ", len(buildTitle)) 27 | 28 | n, _ := strconv.ParseInt(h.BuildTime, 10, 64) 29 | buildTimeStr := time.Unix(n, 0).Format(time.UnixDate) 30 | 31 | buildTmpl := fmt.Sprintf("\n%s\n", buildTitle) + 32 | fmt.Sprintf("%s revision %s\n", tab, h.BuildRevision) + 33 | fmt.Sprintf("%s datetime %s\n", tab, buildTimeStr) 34 | 35 | if h.ShowGoRuntimeVersion { 36 | buildTmpl += fmt.Sprintf("%s runtime %s\n", tab, runtime.Version()) 37 | } 38 | 39 | tmpl += buildTmpl 40 | } 41 | 42 | return tmpl 43 | } 44 | -------------------------------------------------------------------------------- /parser/modpkg_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestPackage(t *testing.T) { 11 | contents := []byte(`package main 12 | func main() {}`) 13 | 14 | if expected, got := []byte("main"), Package(contents); !bytes.Equal(expected, got) { 15 | t.Fatalf("expected %q but got %q", expected, got) 16 | } 17 | } 18 | 19 | func TestModulePath(t *testing.T) { 20 | contents := []byte(`module testmodule 21 | require github.com/kataras/iris/v12 v12.1.5 22 | `) 23 | 24 | if expected, got := []byte("testmodule"), ModulePath(contents); !bytes.Equal(expected, got) { 25 | t.Fatalf("expected %q but got %q", expected, got) 26 | } 27 | } 28 | 29 | func TestTryFindPackage(t *testing.T) { 30 | contents := []byte(`package main 31 | func main() {}`) 32 | 33 | f, err := ioutil.TempFile("", "*.go") 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | if _, err = f.Write(contents); err != nil { 39 | t.Fatal(err) 40 | } 41 | if err = f.Close(); err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | dir := filepath.Dir(f.Name()) 46 | 47 | if expected, got := []byte("main"), TryFindPackage(dir); !bytes.Equal(expected, got) { 48 | t.Fatalf("expected %q but got %q", expected, got) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /project/registry_test.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func TestRegistry(t *testing.T) { 11 | var ( 12 | expected = &Registry{Projects: map[string]string{ 13 | "iris": "github.com/kataras/iris", 14 | "neffos": "github.com/kataras/neffos", 15 | "neffos.js": "github.com/kataras/neffos.js", 16 | }} 17 | 18 | tests = []func(*Registry) *Registry{ 19 | newTestRegistryEndpointAsset, 20 | } 21 | ) 22 | 23 | for _, tt := range tests { 24 | reg := tt(expected) 25 | if err := reg.Load(); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if expected, got := len(expected.Projects), len(reg.Projects); expected != got { 30 | t.Fatalf("expected length of projects: %d but got %d", expected, got) 31 | } 32 | 33 | for name := range reg.Projects { 34 | if expected, got := expected.Projects[name], reg.Projects[name]; !reflect.DeepEqual(expected, got) { 35 | t.Fatalf("project [%s] failed to load: expected:\n%#+v\nbut got\n%#+v", name, expected, got) 36 | } 37 | } 38 | } 39 | } 40 | 41 | func newTestRegistryEndpointAsset(expectedProjects *Registry) *Registry { 42 | reg := NewRegistry() 43 | reg.Endpoint = "./test.yml" 44 | reg.EndpointAsset = func(string) ([]byte, error) { 45 | return yaml.Marshal(expectedProjects) 46 | } 47 | return reg 48 | } 49 | -------------------------------------------------------------------------------- /utils/cmd_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows && !appengine 2 | // +build windows,!appengine 3 | 4 | package utils 5 | 6 | import ( 7 | "context" 8 | "io" 9 | "os/exec" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | ) 14 | 15 | // Command returns the Cmd struct to execute the named program with 16 | // the given arguments for windows. 17 | func Command(name string, args ...string) *exec.Cmd { 18 | cmd := exec.Command(name, args...) 19 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 20 | return cmd 21 | } 22 | 23 | // CommandWithCancel same as `Command` but returns a canceletion function too. 24 | func CommandWithCancel(name string, args ...string) (*exec.Cmd, context.CancelFunc) { 25 | ctx, cancelFunc := context.WithCancel(context.Background()) 26 | cmd := exec.CommandContext(ctx, name, args...) 27 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 28 | return cmd, func() { 29 | if cmd != nil { 30 | if cmd.ProcessState == nil { // it's not already closed. 31 | if cmd.Process != nil && cmd.Process.Pid > 0 { 32 | // println("Killing: " + name + strings.Join(args, " ")) 33 | _ = KillCommand(cmd) 34 | } 35 | } 36 | 37 | cancelFunc() 38 | } 39 | } 40 | } 41 | 42 | func KillCommand(cmd *exec.Cmd) error { 43 | kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)) 44 | return kill.Run() 45 | } 46 | 47 | func FormatExecutable(bin string) string { 48 | if ext := ".exe"; !strings.HasSuffix(bin, ext) { 49 | bin += ext 50 | } 51 | 52 | return bin 53 | } 54 | 55 | func StartExecutable(dir, bin string, stdout, stderr io.Writer) (*exec.Cmd, error) { 56 | cmd := Command("cmd", "/c", bin) 57 | // cmd, cancelFunc := CommandWithCancel(bin) // here the cmd.Process.Pid will give the program's correct PID 58 | cmd.Dir = dir 59 | cmd.Stdout = stdout 60 | cmd.Stderr = stderr 61 | return cmd, cmd.Start() 62 | } 63 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/iris-cli/project" 7 | 8 | "github.com/AlecAivazis/survey/v2" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // iris-cli --time-format=http -v run basic 13 | func runCommand() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "run", 16 | Short: "Run starts a project", 17 | SilenceErrors: true, 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | name := "." // current directory. 20 | if len(args) > 0 { 21 | name = args[0] 22 | } 23 | 24 | p, err := project.LoadFromDisk(name) 25 | if err != nil { 26 | if err == project.ErrProjectNotExists { 27 | p, err = project.LoadFromDisk(".") 28 | if err == nil && p.Name == name { 29 | // argument is not a path but a project name which exists in the current dir. 30 | // ./myproject -> empty 31 | // iris-cli run svelte -> installs and builds the project 32 | // and again iris-cli run svelte -> it's not a directory but it's a project which meant to be ran. 33 | } else { 34 | doInstall := true 35 | err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("%s does not exist, do you want to install it?", name), Default: doInstall}, &doInstall) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if doInstall { 41 | if err := RunCommand(cmd, "new", name /* , "--registry=./_testfiles/registry.yml" */); err != nil { 42 | return err 43 | } 44 | 45 | p, err = project.LoadFromDisk(".") 46 | } else { 47 | return nil 48 | } 49 | } 50 | 51 | } 52 | } 53 | 54 | if err != nil { 55 | if err == project.ErrProjectFileNotExist { 56 | cmd.Println(err) 57 | cmd.Println("run iris-cli init command to create a new one") 58 | return nil 59 | } 60 | return err 61 | } 62 | 63 | return p.Run(cmd.OutOrStdout(), cmd.ErrOrStderr()) 64 | }, 65 | } 66 | 67 | return cmd 68 | } 69 | -------------------------------------------------------------------------------- /utils/cmd_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package utils 5 | 6 | import ( 7 | "context" 8 | "io" 9 | "os/exec" 10 | "path" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/creack/pty" 15 | ) 16 | 17 | // Command returns the Cmd struct to execute the named program with 18 | // the given arguments for windows. 19 | func Command(name string, args ...string) *exec.Cmd { 20 | cmd := exec.Command(name, args...) 21 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 22 | return cmd 23 | } 24 | 25 | // CommandWithCancel same as `Command` but returns a canceletion function too. 26 | func CommandWithCancel(name string, args ...string) (*exec.Cmd, context.CancelFunc) { 27 | ctx, cancelFunc := context.WithCancel(context.TODO()) 28 | cmd := exec.CommandContext(ctx, name, args...) 29 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 30 | return cmd, cancelFunc 31 | } 32 | 33 | func KillCommand(cmd *exec.Cmd) error { 34 | return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 35 | } 36 | 37 | func FormatExecutable(bin string) string { return bin } 38 | 39 | func StartExecutable(dir, bin string, stdout, stderr io.Writer) (*exec.Cmd, error) { 40 | if IsInsideDocker() { 41 | // If run through docker, this part is required, 42 | // otherwise we should NOT try this because it always gives error: 43 | cmd := Command("/bin/sh", "-c", bin) 44 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // set parent group id in order to be kill-able. 45 | cmd.Dir = dir 46 | cmd.Stdout = stdout 47 | cmd.Stderr = stderr 48 | _, err := pty.Start(cmd) 49 | // fork/exec /bin/sh: operation not permitted, even without setpgid. 50 | 51 | if err != nil { 52 | if !strings.Contains(err.Error(), "operation not permitted") { 53 | return nil, err 54 | } 55 | } 56 | } 57 | 58 | cmd := Command(path.Join(dir, bin)) 59 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 60 | cmd.Dir = dir 61 | cmd.Stdout = stdout 62 | cmd.Stderr = stderr 63 | if err := cmd.Start(); err != nil { 64 | return nil, err 65 | } 66 | 67 | return cmd, nil 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kataras/iris-cli 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.7 7 | github.com/cheggaaa/pb/v3 v3.1.5 8 | github.com/creack/pty v1.1.23 9 | github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 10 | github.com/dustin/go-humanize v1.0.1 11 | github.com/fsnotify/fsnotify v1.7.0 12 | github.com/kataras/golog v0.1.12 13 | github.com/kataras/neffos v0.0.23 14 | github.com/spf13/cobra v1.8.1 15 | golang.org/x/sync v0.8.0 16 | gopkg.in/src-d/go-git.v4 v4.13.1 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | github.com/Microsoft/go-winio v0.6.2 // indirect 22 | github.com/VividCortex/ewma v1.2.0 // indirect 23 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect 24 | github.com/emirpasic/gods v1.18.1 // indirect 25 | github.com/fatih/color v1.17.0 // indirect 26 | github.com/gobwas/httphead v0.1.0 // indirect 27 | github.com/gobwas/pool v0.2.1 // indirect 28 | github.com/gobwas/ws v1.4.0 // indirect 29 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 30 | github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect 31 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 32 | github.com/kataras/pio v0.0.13 // indirect 33 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 34 | github.com/kevinburke/ssh_config v1.2.0 // indirect 35 | github.com/mattn/go-colorable v0.1.13 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/mattn/go-runewidth v0.0.16 // indirect 38 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 39 | github.com/mitchellh/go-homedir v1.1.0 // indirect 40 | github.com/rivo/uniseg v0.4.7 // indirect 41 | github.com/sergi/go-diff v1.3.1 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | github.com/src-d/gcfg v1.4.0 // indirect 44 | github.com/xanzy/ssh-agent v0.3.3 // indirect 45 | golang.org/x/crypto v0.27.0 // indirect 46 | golang.org/x/net v0.29.0 // indirect 47 | golang.org/x/sys v0.25.0 // indirect 48 | golang.org/x/term v0.24.0 // indirect 49 | golang.org/x/text v0.18.0 // indirect 50 | gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect 51 | gopkg.in/warnings.v0 v0.1.2 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestParse(t *testing.T) { 10 | src := `package main 11 | 12 | const assetsDirectory = "./app/build_var" 13 | 14 | func main(){ 15 | app := iris.New() 16 | 17 | /* $ command1 18 | $ command2 */ 19 | 20 | // $ command3 21 | 22 | /* $ command arg 23 | $ command5 24 | $ command6 25 | */ 26 | 27 | app.HandleDir("/", "./app/build_literal", iris.DirOptions{ 28 | Asset: Asset, 29 | AssetNames: AssetNames, 30 | AssetInfo: AssetInfo, 31 | }) 32 | 33 | // $ command arg1 arg2 34 | app.HandleDir("/", assetsDirectory, iris.DirOptions{ 35 | Asset: Asset, 36 | AssetNames: AssetNames, 37 | AssetInfo: AssetInfo, 38 | }) 39 | 40 | app.HandleDir("/", "./public") 41 | } 42 | ` 43 | res, err := Parse(src) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | expectedCommands := []string{ 49 | "command1", 50 | "command2", 51 | "command3", 52 | "command arg", 53 | "command5", 54 | "command6", 55 | "command arg1 arg2", 56 | } 57 | 58 | for i, cmd := range res.Commands { 59 | nameArgs := strings.Split(expectedCommands[i], " ") 60 | 61 | if expected, got := nameArgs[0], cmd.Name; expected != got { 62 | t.Fatalf("[%d] expected parsed command to be: %s but got: %s", i, expected, got) 63 | } 64 | 65 | if expected, got := len(nameArgs[1:]), len(cmd.Args); expected != got { 66 | t.Fatalf("[%d] expected parsed command args length to be: %d but got: %d", i, expected, got) 67 | } 68 | 69 | if expected, got := strings.Join(nameArgs[1:], " "), strings.Join(cmd.Args, " "); !reflect.DeepEqual(expected, got) { 70 | t.Fatalf("[%d] expected parsed command args to be: %s but got: %s", i, expected, got) 71 | } 72 | 73 | } 74 | 75 | expectedAssetDirs := []*AssetDir{ 76 | {Dir: "./app/build_literal", ShouldGenerated: true}, 77 | {Dir: "./app/build_var", ShouldGenerated: true}, 78 | {Dir: "./public", ShouldGenerated: false}, 79 | } 80 | if !reflect.DeepEqual(res.AssetDirs, expectedAssetDirs) { 81 | t.Fatalf("expected parsed asset targets to be:\n<%v>\nbut got:\n<%v>", 82 | res.AssetDirs, expectedAssetDirs) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /utils/interrupt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "sync" 7 | "syscall" 8 | ) 9 | 10 | // RegisterOnInterrupt registers a global function to call when CTRL+C/CMD+C pressed or a unix kill command received. 11 | func RegisterOnInterrupt(cb func()) { 12 | Interrupt.Register(cb) 13 | } 14 | 15 | // Interrupt watches the os.Signals for interruption signals 16 | // and fires the callbacks when those happens. 17 | // A call of its `FireNow` manually will fire and reset the registered interrupt handlers. 18 | var Interrupt = new(interruptListener) 19 | 20 | type interruptListener struct { 21 | mu sync.Mutex 22 | once sync.Once 23 | // onInterrupt contains a list of the functions that should be called when CTRL+C/CMD+C or 24 | // a unix kill command received. 25 | onInterrupt []func() 26 | } 27 | 28 | // Register registers a global function to call when CTRL+C/CMD+C pressed or a unix kill command received. 29 | func (i *interruptListener) Register(cb func()) { 30 | if cb == nil { 31 | return 32 | } 33 | 34 | i.listenOnce() 35 | i.mu.Lock() 36 | i.onInterrupt = append(i.onInterrupt, cb) 37 | i.mu.Unlock() 38 | } 39 | 40 | // FireNow can be called more than one times from a Consumer in order to 41 | // execute all interrupt handlers manually. 42 | func (i *interruptListener) FireNow() { 43 | i.mu.Lock() 44 | for _, f := range i.onInterrupt { 45 | f() 46 | } 47 | i.onInterrupt = i.onInterrupt[0:0] 48 | i.mu.Unlock() 49 | } 50 | 51 | // listenOnce fires a goroutine which calls the interrupt handlers when CTRL+C/CMD+C and e.t.c. 52 | // If `FireNow` called before then it does nothing when interrupt signal received, 53 | // so it's safe to be used side by side with `FireNow`. 54 | // 55 | // Btw this `listenOnce` is called automatically on first register, it's useless for outsiders. 56 | func (i *interruptListener) listenOnce() { 57 | i.once.Do(i.notifyAndFire) 58 | } 59 | 60 | func (i *interruptListener) notifyAndFire() { 61 | ch := make(chan os.Signal, 2) 62 | signal.Notify(ch, 63 | // kill -SIGINT XXXX or Ctrl+c 64 | os.Interrupt, 65 | syscall.SIGINT, // register that too, it should be ok 66 | // os.Kill is equivalent with the syscall.SIGKILL 67 | // os.Kill, 68 | // syscall.SIGKILL, // register that too, it should be ok 69 | // kill -SIGTERM XXXX 70 | syscall.SIGTERM, 71 | ) 72 | go func() { 73 | <-ch 74 | i.FireNow() 75 | os.Exit(0) 76 | }() 77 | } 78 | -------------------------------------------------------------------------------- /project/registry.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/url" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/kataras/iris-cli/utils" 11 | 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | const DefaultRegistryEndpoint = "https://raw.githubusercontent.com/kataras/iris-cli/main/registry.yml" 16 | 17 | type Registry struct { 18 | Endpoint string `json:"endpoint,omitempty" yaml:"Endpoint" toml:"Endpoint"` 19 | EndpointAsset func(string) ([]byte, error) `json:"-" yaml:"-" toml:"-"` // If EndpointAsset is not nil then it reads the Endpoint from that `EndpointAsset` function. 20 | Projects map[string]string `json:"projects" yaml:"Projects" toml:"Projects"` // key = name, value = repo. 21 | installed map[string]struct{} 22 | Names []string `json:"-" yaml:"-" toml:"-"` // sorted Projects names. 23 | } 24 | 25 | func NewRegistry() *Registry { 26 | return &Registry{ 27 | Endpoint: DefaultRegistryEndpoint, 28 | Projects: make(map[string]string), 29 | installed: make(map[string]struct{}), 30 | } 31 | } 32 | 33 | func (r *Registry) Load() error { 34 | var ( 35 | body []byte 36 | err error 37 | ) 38 | 39 | if r.EndpointAsset != nil { 40 | body, err = r.EndpointAsset(r.Endpoint) 41 | } else { 42 | if isURL := strings.HasPrefix(r.Endpoint, "http"); isURL { 43 | if _, urlErr := url.Parse(r.Endpoint); urlErr != nil { 44 | return err 45 | } 46 | body, err = utils.Download(r.Endpoint, nil) 47 | } else { 48 | body, err = ioutil.ReadFile(r.Endpoint) 49 | } 50 | } 51 | 52 | if err != nil { 53 | return err 54 | } 55 | 56 | switch ext := utils.Ext(r.Endpoint); ext { 57 | case ".yaml", ".yml": 58 | err = yaml.Unmarshal(body, r) 59 | default: 60 | err = fmt.Errorf("unknown registry file extension: %s", ext) 61 | } 62 | 63 | if err != nil { 64 | return err 65 | } 66 | 67 | names := make([]string, 0, len(r.Projects)) 68 | for name := range r.Projects { 69 | names = append(names, name) 70 | } 71 | sort.Strings(names) 72 | r.Names = names 73 | return nil 74 | } 75 | 76 | // ErrProjectNotExists can be return as error value from the `Registry.Install` method. 77 | var ErrProjectNotExists = fmt.Errorf("project does not exist") 78 | 79 | // Exists reports whether a project with "name" exists in the registry. 80 | func (r *Registry) Exists(name string) (string, bool) { 81 | repo, ok := r.Projects[name] 82 | return repo, ok 83 | } 84 | 85 | // Install downloads and unzips a project with "name" to "dest" as "module". 86 | func (r *Registry) Install(p *Project) error { 87 | for projectName, repo := range r.Projects { 88 | if projectName != p.Name { 89 | continue 90 | } 91 | 92 | p.Repo = repo 93 | 94 | err := p.Install() 95 | if err == nil { 96 | r.installed[projectName] = struct{}{} 97 | } 98 | return err 99 | } 100 | 101 | return ErrProjectNotExists 102 | } 103 | -------------------------------------------------------------------------------- /project/livereload.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/neffos" 8 | "github.com/kataras/neffos/gobwas" 9 | ) 10 | 11 | type LiveReload struct { 12 | // Disable set to true to disable browser live reload. 13 | Disable bool `json:"disable" yaml:"Disable" toml:"Disable"` 14 | 15 | // No, the server should have the localhost everywhere, accept just the port. 16 | // // Addr is the host:port address of the websocket server. 17 | // // The javascript file which listens on updates and should be included on the application 18 | // // is served through: {Addr}/livereload.js. 19 | // // The websocket endpoint is {Addr}/livereload. 20 | // // 21 | // // Defaults to :35729. 22 | // Addr string `json:"addr" yaml:"Addr" toml:"Addr"` 23 | Port int `json:"port" yaml:"Port" toml:"Port"` 24 | ws *neffos.Server 25 | } 26 | 27 | func NewLiveReload() *LiveReload { 28 | return &LiveReload{ 29 | Port: 35729, 30 | } 31 | } 32 | 33 | func (l *LiveReload) ListenAndServe() error { 34 | if l.Disable { 35 | return nil 36 | } 37 | 38 | if l.Port <= 0 { 39 | return nil 40 | } 41 | 42 | l.ws = neffos.New(gobwas.DefaultUpgrader, neffos.Events{ 43 | // Register OnNativeMessage on empty namespace. 44 | // Communicatation with this server can happen only through browser's native websocket API. 45 | neffos.OnNativeMessage: func(c *neffos.NSConn, msg neffos.Message) error { 46 | return nil 47 | }}) 48 | 49 | mux := http.NewServeMux() 50 | mux.Handle("/livereload", l.ws) 51 | mux.HandleFunc("/livereload.js", l.HandleJS()) 52 | 53 | return http.ListenAndServe(fmt.Sprintf(":%d", l.Port), mux) 54 | } 55 | 56 | var reloadMessage = neffos.Message{IsNative: true, Body: []byte("full_reload")} 57 | 58 | func (l *LiveReload) SendReloadSignal() { 59 | if l.Disable { 60 | return 61 | } 62 | 63 | l.ws.Broadcast(nil, reloadMessage) 64 | } 65 | 66 | // HandleJS serves the /livereload.js. 67 | // 68 | // We handle the javascript side here in order to be 69 | // easier to listen on reload events within any application. 70 | // 71 | // Just add this script before the closing body tag: