├── partials ├── go.mod ├── lib-call.go ├── lib-args.go ├── test-cli-args.txt ├── go-helpers.go ├── pflag-init.go ├── args-parse.go └── flag-init.go ├── templates ├── go.mod ├── main.go ├── hls │ └── cli │ │ ├── root_help.hls │ │ └── cmd_help.hls ├── verinfo.go ├── Dockerfile.cue ├── root_test.go ├── Makefile ├── alt │ ├── box.go │ ├── box-gen.go │ └── goreleaser.yml ├── version.go ├── cmd_test.go ├── flags.go ├── completions.go ├── ga.go ├── root.go ├── cmd.go └── update.go ├── creators ├── templates │ ├── cue.mod │ │ └── module.cue │ ├── Makefile │ └── cli.cue └── creator.cue ├── hof.cue ├── cue.mod └── module.cue ├── schema ├── cmd.cue ├── arg.cue ├── flag.cue ├── release.cue ├── cli.cue └── common.cue ├── .gitignore ├── LICENSE ├── README.md └── gen └── cli.cue /partials/go.mod: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/go.mod: -------------------------------------------------------------------------------- 1 | module {{ .CLI.Module }} 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /templates/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "{{ .CLI.Package }}/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.RunExit() 9 | } 10 | -------------------------------------------------------------------------------- /partials/lib-call.go: -------------------------------------------------------------------------------- 1 | {{ if .CMD.Args -}} 2 | {{ range $i, $A := .CMD.Args -}} 3 | {{ if gt $i 0 }}, {{end }}{{ $A.argName -}} 4 | {{ end -}} 5 | {{ else -}} 6 | args{{ end -}} 7 | -------------------------------------------------------------------------------- /partials/lib-args.go: -------------------------------------------------------------------------------- 1 | {{ if .CMD.Args -}} 2 | {{ range $i, $A := .CMD.Args -}} 3 | {{ if gt $i 0 }}, {{end }}{{ $A.argName }} {{ $A.Type -}} 4 | {{ end -}} 5 | {{ else -}} 6 | args []string{{ end -}} 7 | -------------------------------------------------------------------------------- /creators/templates/cue.mod/module.cue: -------------------------------------------------------------------------------- 1 | module: "{{ .repo }}" 2 | "cue": "0.6.0" 3 | 4 | require: { 5 | "github.com/hofstadter-io/hofmod-cli": "v0.8.9" 6 | "github.com/hofstadter-io/hof": "v0.6.8" 7 | } 8 | -------------------------------------------------------------------------------- /hof.cue: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/hofstadter-io/hofmod-cli/creators" 5 | "github.com/hofstadter-io/hofmod-cli/gen" 6 | ) 7 | 8 | Creator: creators.Creator 9 | Generator: gen.Geneartor 10 | -------------------------------------------------------------------------------- /cue.mod/module.cue: -------------------------------------------------------------------------------- 1 | module: "github.com/hofstadter-io/hofmod-cli" 2 | language: { 3 | version: "v0.13.0" 4 | } 5 | source: { 6 | kind: "git" 7 | } 8 | deps: { 9 | "github.com/hofstadter-io/schemas@v0": { 10 | v: "v0.0.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /creators/templates/Makefile: -------------------------------------------------------------------------------- 1 | # todo, turn this into a flow on the generator? 2 | # either post or manually run 3 | 4 | .PHONY: first 5 | first: 6 | rm Makefile 7 | hof mod tidy 8 | hof gen 9 | go mod tidy 10 | go build -o {{ .name }} ./cmd/{{ .name }} 11 | ./{{ .name }} -h 12 | -------------------------------------------------------------------------------- /schema/cmd.cue: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | Command: { 8 | Name: string 9 | cmdName: strings.ToCamel(Name) 10 | CmdName: strings.ToTitle(Name) 11 | 12 | Hidden?: bool 13 | OmitTests?: bool 14 | Aliases?: [...string] 15 | PackageName?: string 16 | 17 | Common 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | /cue.mod/pkg/ 17 | -------------------------------------------------------------------------------- /schema/arg.cue: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | ArgType: 8 | *"string" | 9 | "[]string" | 10 | "int" 11 | 12 | Arg: { 13 | Name: string 14 | argName: strings.ToCamel(Name) 15 | ArgName: strings.ToTitle(Name) 16 | 17 | Type: ArgType 18 | Default?: _ 19 | Required?: bool 20 | Rest?: bool 21 | Help: string | *"" 22 | } 23 | -------------------------------------------------------------------------------- /templates/hls/cli/root_help.hls: -------------------------------------------------------------------------------- 1 | ### Test "{{ .CLI.cliName }} --help" prints help 2 | call __{{ .CLI.cliName }} --help 3 | 4 | ### Test "{{ .CLI.cliName }} -h" prints help 5 | call __{{ .CLI.cliName }} -h 6 | 7 | ### Test "{{ .CLI.cliName }} help" prints help 8 | call __{{ .CLI.cliName }} help 9 | 10 | {{ if .CLI.OmitRun }} 11 | ### Test "{{ .CLI.cliName }}" (without any args) prints help 12 | call __{{ .CLI.cliName }} 13 | {{end}} 14 | -------------------------------------------------------------------------------- /schema/flag.cue: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | FlagType: 8 | *"string" | "[]string" | 9 | "int" | "[]int" | 10 | "float64" | "[]float64" | 11 | "bool" 12 | 13 | Flag: { 14 | Name: string 15 | flagName: strings.ToCamel(Name) 16 | FlagName: strings.ToTitle(Name) 17 | 18 | Type: FlagType 19 | Default: _ 20 | Help: string | *"" 21 | Long: string | *"" 22 | Short: string | *"" 23 | } 24 | -------------------------------------------------------------------------------- /templates/hls/cli/cmd_help.hls: -------------------------------------------------------------------------------- 1 | ### Test "{{ .CMD.cmdName }} --help" prints help 2 | call __{{ .CLI.cliName }} {{ template "test-cli-args.txt" . }} --help 3 | 4 | ### Test "{{ .CMD.cmdName }} -h" prints help 5 | call __{{ .CLI.cliName }} {{ template "test-cli-args.txt" . }} -h 6 | 7 | {{ if .CMD.OmitRun -}} 8 | ### Test "{{ .CMD.cmdName }}" (without any args) prints help 9 | call __{{ .CLI.cliName }} {{ template "test-cli-args.txt" . }} 10 | {{end}} 11 | -------------------------------------------------------------------------------- /schema/release.cue: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | GoReleaser: { 4 | Disable: bool | *true 5 | Draft: bool | *true 6 | Author: string 7 | Homepage: string 8 | 9 | GitHub: { 10 | Owner: string 11 | Repo: string 12 | URL: "https://github.com/\(Owner)/\(Repo)" 13 | Rel: "https://api.github.com/repos/\(Owner)/\(Repo)/releases" 14 | } 15 | 16 | Docker: { 17 | Maintainer: string 18 | Repo: string 19 | Versions: { 20 | debian: string | *"11.3" 21 | alpine: string | *"3.15" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /partials/test-cli-args.txt: -------------------------------------------------------------------------------- 1 | {{- if .CMD.Commands -}} 2 | {{- if .CMD.Parent.Parent.Parent.Parent -}} 3 | {{- .CMD.Parent.Parent.Parent.Parent.Name }} {{ .CMD.Parent.Parent.Parent.Name }} {{ .CMD.Parent.Parent.Name }} {{ .CMD.Parent.Name }} {{ .CMD.cmdName -}} 4 | {{- else if .CMD.Parent.Parent.Parent -}} 5 | {{- .CMD.Parent.Parent.Parent.Name }} {{ .CMD.Parent.Parent.Name }} {{ .CMD.Parent.Name }} {{ .CMD.cmdName -}} 6 | {{- else if .CMD.Parent.Parent -}} 7 | {{- .CMD.Parent.Parent.Name }} {{ .CMD.Parent.Name }} {{ .CMD.cmdName -}} 8 | {{- else if .CMD.Parent -}} 9 | {{- .CMD.Parent.Name }} {{ .CMD.cmdName -}} 10 | {{- else -}} 11 | {{- .CMD.cmdName -}} 12 | {{- end -}} 13 | {{- else -}} 14 | {{- .CMD.cmdName -}} 15 | {{- end -}} 16 | 17 | -------------------------------------------------------------------------------- /partials/go-helpers.go: -------------------------------------------------------------------------------- 1 | {{- define "cobra-type" -}} 2 | {{- if eq . "string"}}String 3 | {{- else if eq . "[]string" }}StringArray 4 | {{- else if eq . "int" }}Int 5 | {{- else if eq . "[]int" }}IntSlice 6 | {{- else if eq . "float64" }}Float64 7 | {{- else if eq . "[]float64" }}Float64Slice 8 | {{- else if eq . "bool" }}Bool 9 | {{- else }} 10 | {{- end -}} 11 | {{- end -}} 12 | 13 | {{- define "go-default" -}} 14 | {{- if eq . "string"}}"" 15 | {{- else if eq . "[]string" }}[]string{} 16 | {{- else if eq . "int" }}0 17 | {{- else if eq . "[]int" }}[]int{} 18 | {{- else if eq . "float64" }}0.0 19 | {{- else if eq . "[]float64" }}[]float64P{} 20 | {{- else if eq . "bool" }}false 21 | {{- else }} 22 | {{- end -}} 23 | {{- end -}} 24 | -------------------------------------------------------------------------------- /templates/verinfo.go: -------------------------------------------------------------------------------- 1 | package verinfo 2 | 3 | import ( 4 | "runtime" 5 | "runtime/debug" 6 | ) 7 | 8 | var ( 9 | Version = "Local" 10 | Commit = "Dirty" 11 | 12 | BuildDate = "Unknown" 13 | GoVersion = "Unknown" 14 | BuildOS = "Unknown" 15 | BuildArch = "Unknown" 16 | 17 | ) 18 | 19 | func init() { 20 | info, _ := debug.ReadBuildInfo() 21 | GoVersion = info.GoVersion 22 | 23 | if Version == "Local" { 24 | BuildOS = runtime.GOOS 25 | BuildArch = runtime.GOARCH 26 | 27 | dirty := false 28 | for _, s := range info.Settings { 29 | if s.Key == "vcs.revision" { 30 | Commit = s.Value 31 | } 32 | if s.Key == "vcs.time" { 33 | BuildDate = s.Value 34 | } 35 | if s.Key == "vcs.modified" { 36 | if s.Value == "true" { 37 | dirty = true 38 | } 39 | } 40 | } 41 | if dirty { 42 | Commit += "+dirty" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /templates/Dockerfile.cue: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | 4 | DockerfileMaintainer: """ 5 | MAINTAINER {{ .CLI.Releases.Docker.Maintainer }} 6 | """ 7 | 8 | DockerfileWorkdir: """ 9 | VOLUME ["/work"] 10 | WORKDIR /work 11 | """ 12 | 13 | 14 | DockerfileDebian: """ 15 | FROM debian:{{ .CLI.Releases.Docker.Versions.debian }} 16 | \(DockerfileMaintainer) 17 | 18 | COPY {{ .CLI.cliName }} /usr/local/bin 19 | ENTRYPOINT ["{{ .CLI.cliName }}"] 20 | 21 | \(DockerfileWorkdir) 22 | 23 | """ 24 | 25 | DockerfileAlpine: """ 26 | FROM alpine:{{ .CLI.Releases.Docker.Versions.alpine }} 27 | \(DockerfileMaintainer) 28 | 29 | COPY {{ .CLI.cliName }} /usr/local/bin 30 | ENTRYPOINT ["{{ .CLI.cliName }}"] 31 | 32 | \(DockerfileWorkdir) 33 | 34 | """ 35 | 36 | 37 | DockerfileScratch: """ 38 | FROM scratch 39 | \(DockerfileMaintainer) 40 | 41 | COPY {{ .CLI.cliName }} / 42 | ENTRYPOINT ["/{{ .CLI.cliName }}"] 43 | 44 | \(DockerfileWorkdir) 45 | 46 | """ 47 | 48 | -------------------------------------------------------------------------------- /templates/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hofstadter-io/hof/script/runtime" 7 | "github.com/hofstadter-io/hof/lib/yagu" 8 | 9 | "{{ .CLI.Package }}/cmd" 10 | ) 11 | 12 | func init() { 13 | // ensure our root command is setup 14 | cmd.RootInit() 15 | } 16 | 17 | func TestScriptRootCliTests(t *testing.T) { 18 | // setup some directories 19 | workdir := ".workdir/cli/root" 20 | yagu.Mkdir(workdir) 21 | 22 | runtime.Run(t, runtime.Params{ 23 | Setup: func (env *runtime.Env) error { 24 | // add any environment variables for your tests here 25 | {{ if .CLI.Telemetry }} 26 | env.Vars = append(env.Vars, "{{ .CLI.CLI_NAME }}_TELEMETRY_DISABLED=1") 27 | {{ end }} 28 | return nil 29 | }, 30 | Funcs: map[string] func (ts* runtime.Script, args[]string) error { 31 | "__{{ .CLI.cliName }}": cmd.CallTS, 32 | }, 33 | Dir: "hls/cli/root", 34 | WorkdirRoot: workdir, 35 | }) 36 | } 37 | 38 | -------------------------------------------------------------------------------- /schema/cli.cue: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | Cli: { 8 | Name: string 9 | cliName: strings.ToCamel(Name) 10 | CliName: strings.ToTitle(Name) 11 | CLI_NAME: strings.ToUpper(Name) 12 | 13 | Module: string 14 | Package: string | *"\(Module)/cmd/\(Name)" 15 | 16 | Updates: bool | *true 17 | VersionCommand: bool | *true 18 | CompletionCommands: bool | *true 19 | 20 | Releases?: GoReleaser 21 | 22 | ConfigDir: string | *"\(cliName)" 23 | 24 | // directory of files to embed into the binary 25 | EmbedDir?: string 26 | 27 | Telemetry?: string 28 | TelemetryAsk: string | *""" 29 | We only send the command run, no args or input. 30 | You can disable at any time by setting 31 | \(CLI_NAME)_TELEMETRY_DISABLED=1 32 | 33 | Would you like to help by sharing very basic usage stats? 34 | """ 35 | // subdir under os.UserConfigDir 36 | TelemetryIdDir: string | *"\(cliName)" 37 | 38 | // Debugging 39 | EnablePProf: bool | *false 40 | 41 | Common 42 | } 43 | -------------------------------------------------------------------------------- /partials/pflag-init.go: -------------------------------------------------------------------------------- 1 | {{ define "pflag-init" }} 2 | {{ if $.Pflags}} 3 | 4 | {{ $CmdName := "Root" }} 5 | {{ if $.CmdName }} 6 | {{ $CmdName = $.CmdName }} 7 | {{ end }} 8 | 9 | {{ $Prefix := "Root" }} 10 | {{ if $.Parent.Parent.Parent.Parent }} 11 | {{ $Prefix = (print $.Parent.Parent.Parent.Parent.Name "__" $.Parent.Parent.Parent.Name "__" $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 12 | {{ else if $.Parent.Parent.Parent }} 13 | {{ $Prefix = (print $.Parent.Parent.Parent.Name "__" $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 14 | {{ else if $.Parent.Parent }} 15 | {{ $Prefix = (print $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 16 | {{ else if $.Parent }} 17 | {{ $Prefix = (print $.Parent.Name "__" $.CmdName) }} 18 | {{ else if $.CmdName }} 19 | {{ $Prefix = $.CmdName }} 20 | {{ end }} 21 | {{ $Prefix = ( title $Prefix ) }} 22 | 23 | func init () { 24 | {{- range $i, $F := $.Pflags }} 25 | {{ $CmdName }}Cmd.PersistentFlags().AddFlag(flags.{{ $Prefix }}FlagSet.Lookup("{{ $F.Long }}")) 26 | {{- end }} 27 | } 28 | {{ end }} 29 | {{ end }} 30 | -------------------------------------------------------------------------------- /partials/args-parse.go: -------------------------------------------------------------------------------- 1 | {{ define "args-parse" }} 2 | {{ $ARGS := . }} 3 | // Argument Parsing 4 | {{ range $i, $A := $ARGS }} 5 | {{ if $A.Required }} 6 | if {{ $i }} >= len(args) { 7 | fmt.Println("missing required argument: '{{$A.argName}}'") 8 | cmd.Usage() 9 | os.Exit(1) 10 | } 11 | {{ end }} 12 | var {{ $A.argName }} {{$A.Type}} 13 | {{ if $A.Default }}{{ $A.argName }} = {{ $A.Default }}{{end}} 14 | 15 | if {{ $i }} < len(args) { 16 | {{ if $A.Rest }} 17 | {{ $A.argName }} = args[{{ $i }}:] 18 | 19 | {{ else if eq $A.Type "string" }} 20 | {{ $A.argName }} = args[{{ $i }}] 21 | 22 | {{ else if eq $A.Type "int" }} 23 | {{ $A.argName}}Str := args[{{ $i }}] 24 | var {{ $A.argName }}Err error 25 | {{ $A.argName }}Type, {{ $A.argName }}Err := strconv.ParseInt({{ $A.argName }}Str, 10, 64) 26 | if {{ $A.argName }}Err != nil { 27 | fmt.Printf("argument of wrong type. expected: '{{ $A.Type}}' got error: %v", {{ $A.argName }}Err ) 28 | cmd.Usage() 29 | os.Exit(1) 30 | } 31 | {{ $A.argName }} = int({{ $A.argName }}Type) 32 | 33 | {{ end }} 34 | } 35 | {{ end }} 36 | {{ end }} 37 | -------------------------------------------------------------------------------- /partials/flag-init.go: -------------------------------------------------------------------------------- 1 | {{ define "flag-init" }} 2 | {{ if (or $.Pflags $.Flags)}} 3 | 4 | {{ $CmdName := "Root" }} 5 | {{ if $.CmdName }} 6 | {{ $CmdName = $.CmdName }} 7 | {{ end }} 8 | 9 | {{ $Prefix := "Root" }} 10 | {{ if $.Parent.Parent.Parent.Parent }} 11 | {{ $Prefix = (print $.Parent.Parent.Parent.Parent.Name "__" $.Parent.Parent.Parent.Name "__" $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 12 | {{ else if $.Parent.Parent.Parent }} 13 | {{ $Prefix = (print $.Parent.Parent.Parent.Name "__" $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 14 | {{ else if $.Parent.Parent }} 15 | {{ $Prefix = (print $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 16 | {{ else if $.Parent }} 17 | {{ $Prefix = (print $.Parent.Name "__" $.CmdName) }} 18 | {{ else if $.CmdName }} 19 | {{ $Prefix = $.CmdName }} 20 | {{ end }} 21 | {{ $Prefix = ( title $Prefix ) }} 22 | 23 | func init () { 24 | {{ if $.Pflags }} 25 | flags.Setup{{ $Prefix }}Pflags({{ $CmdName }}Cmd.PersistentFlags(), &(flags.{{ $Prefix }}Pflags)) 26 | {{ end }} 27 | {{ if $.Flags }} 28 | flags.Setup{{ $Prefix }}Flags({{ $CmdName }}Cmd.Flags(), &(flags.{{ $Prefix }}Flags)) 29 | {{ end }} 30 | } 31 | {{ end }} 32 | {{ end }} 33 | -------------------------------------------------------------------------------- /schema/common.cue: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "text/template" 4 | 5 | Import: { 6 | As: string | *"" 7 | Path: string 8 | } 9 | 10 | Common: { 11 | Name: string 12 | Usage: string | *Name 13 | Short: string 14 | Long: string | *Short 15 | TBD: string | *"" 16 | 17 | Help: template.Execute("{{ printf \"%-15s %-5s %s\" .name .tbd .short }}", {tbd: TBD, name: Name, short: Short}) 18 | CustomHelp?: string 19 | 20 | PersistentPrerun: bool | *false 21 | Prerun: bool | *false 22 | OmitRun: bool | *false 23 | Postrun: bool | *false 24 | PersistentPostrun: bool | *false 25 | 26 | PersistentPrerunBody?: string 27 | PrerunBody?: string 28 | Body?: string 29 | PostrunBody?: string 30 | PersistentPostrunBody?: string 31 | 32 | HasAnyRun: bool 33 | HasAnyRun: !OmitRun || PersistentPrerun || Prerun || Postrun || PersistentPostrun 34 | 35 | HasAnyFlags: bool 36 | HasAnyFlags: Pflags != _|_ || Flags != _|_ 37 | 38 | Imports?: [...Import] 39 | Pflags?: [...Flag] 40 | Flags?: [...Flag] 41 | Args?: [...Arg] 42 | Commands: [...Command] | *[] 43 | 44 | Topics: [string]: string 45 | Examples: [string]: string 46 | Tutorials: [string]: string 47 | } 48 | -------------------------------------------------------------------------------- /creators/templates/cli.cue: -------------------------------------------------------------------------------- 1 | package {{ .name }} 2 | 3 | import ( 4 | "github.com/hofstadter-io/hofmod-cli/gen" 5 | "github.com/hofstadter-io/hofmod-cli/schema" 6 | ) 7 | 8 | "{{ .name }}": gen.Generator & { 9 | @gen({{.name}},cli) 10 | Name: "{{ .name }}" 11 | Outdir: "./" 12 | Cli: cli 13 | WatchGlobs: ["./*.cue"] 14 | } 15 | 16 | cli: schema.Cli & { 17 | 18 | Name: "{{ .name }}" 19 | Module: "{{ .repo }}" 20 | Package: "{{ .repo }}/cmd/{{ .name }}" 21 | 22 | Usage: "{{ .name }}" 23 | Short: "{{ .about }}" 24 | Long: Short 25 | 26 | // set to true to print help from root command 27 | // and always assume subcommands are to be run 28 | // set to false to run code from the root command 29 | OmitRun: true 30 | 31 | Args: [ 32 | // add any args here 33 | ] 34 | 35 | Commands: [ 36 | // add any subcommands here 37 | ] 38 | 39 | // extras 40 | VersionCommand: true 41 | CompletionCommands: true 42 | {{ if .updates }} 43 | Updates: true 44 | {{ else }} 45 | Updates: false 46 | {{ end }} 47 | 48 | {{ if .telemetry }} 49 | // set your GA identifier here 50 | Telemetry: "ua-xxxxxx" 51 | {{ end }} 52 | 53 | {{ if .releases }} 54 | // GoReleaser configuration 55 | // see https://docs.hofstadter.io/... for options 56 | Releases: { 57 | Author: "" 58 | Homepage: "" 59 | 60 | GitHub: { 61 | Owner: "" 62 | Repo: "" 63 | } 64 | 65 | Docker: { 66 | Maintainer: "" 67 | Repo: "" 68 | } 69 | } 70 | {{ end }} 71 | } 72 | 73 | -------------------------------------------------------------------------------- /templates/Makefile: -------------------------------------------------------------------------------- 1 | CUE_FILES = $(shell find . -type f -name '*.cue' | grep -v 'cue.mod/pkg/' | sort) 2 | GO_FILES = $(shell find . -type f -name '*.go' | grep -v 'cue.mod/pkg/' | sort) 3 | # GHA_FILES = $(shell ls .github/workflows/*.cue | sort) 4 | 5 | # First command incase users forget to supply one 6 | # cat self as help for simplicity 7 | help: 8 | cat Makefile 9 | .PHONY: help 10 | 11 | # GitHub Actions workflows 12 | workflows = $(addprefix workflow_, $(GHA_FILES)) 13 | workflow: $(workflows) 14 | $(workflows): workflow_%: 15 | @cue export --out yaml $(subst workflow_,,$@) > $(subst workflow_,,$(subst .cue,,$@)).yml 16 | .PHONY: workflow 17 | 18 | deps: 19 | hof mod vendor cue 20 | go mod tidy 21 | .PHONY: deps 22 | 23 | build: 24 | CGO_ENABLED=0 go build -o {{ .CLI.Name }} ./cmd/{{ .CLI.Name }} 25 | .PHONY: build 26 | 27 | install: 28 | CGO_ENABLED=0 go install ./cmd/{{ .CLI.Name }} 29 | .PHONY: install 30 | 31 | fmt: fmt.go fmt.cue 32 | fmt.go: 33 | find . -type f -name '*.go' '!' -path '*/cue.mod/*' '!' -path '*/templates/*' '!' -path '*/partials/*' '!' -path '*/.hof/*' -exec gofmt -w {} \; 34 | .PHONY: fmt.go 35 | fmt.cue: 36 | find . -type f -name '*.cue' '!' -path '*/cue.mod/*' '!' -path '*/templates/*' '!' -path '*/partials/*' '!' -path '*/.hof/*' -exec cue fmt {} \; 37 | .PHONY: fmt.cue 38 | 39 | release: 40 | cd cmd/hof && goreleaser --rm-dist -p 1 41 | .PHONY: release 42 | 43 | snapshot: 44 | cd cmd/hof && goreleaser --rm-dist -p 1 --snapshot 45 | .PHONY: snapshot 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Hofstadter, Inc 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /templates/alt/box.go: -------------------------------------------------------------------------------- 1 | //go:generate go run generator.go 2 | 3 | package box 4 | 5 | // from: https://dev.to/koddr/the-easiest-way-to-embed-static-files-into-a-binary-file-in-your-golang-app-no-external-dependencies-43pc 6 | 7 | import "sort" 8 | 9 | type embedBox struct { 10 | storage map[string][]byte 11 | } 12 | 13 | // Create new box for embed files 14 | func newEmbedBox() *embedBox { 15 | return &embedBox{storage: make(map[string][]byte)} 16 | } 17 | 18 | // Add a file to box 19 | func (e *embedBox) Add(file string, content []byte) { 20 | e.storage[file] = content 21 | } 22 | 23 | // Get file's content 24 | // Always use / for looking up 25 | // For example: /init/README.md is actually configs/init/README.md 26 | func (e *embedBox) Get(file string) []byte { 27 | if f, ok := e.storage[file]; ok { 28 | return f 29 | } 30 | return nil 31 | } 32 | 33 | // Find for a file 34 | func (e *embedBox) Has(file string) bool { 35 | if _, ok := e.storage[file]; ok { 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | func (e *embedBox) List() []string { 42 | files := []string{} 43 | for fn, _ := range e.storage { 44 | files = append(files, fn) 45 | } 46 | sort.Strings(files) 47 | return files 48 | } 49 | 50 | // Embed box expose 51 | var box = newEmbedBox() 52 | 53 | // Add a file content to box 54 | func Add(file string, content []byte) { 55 | box.Add(file, content) 56 | } 57 | 58 | // Get a file from box 59 | func Get(file string) []byte { 60 | return box.Get(file) 61 | } 62 | 63 | // Has a file in box 64 | func Has(file string) bool { 65 | return box.Has(file) 66 | } 67 | 68 | // List files in box 69 | func List() []string { 70 | return box.List() 71 | } 72 | 73 | -------------------------------------------------------------------------------- /templates/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | {{ if .CLI.Telemetry }} 11 | "{{ .CLI.Package }}/ga" 12 | {{ end }} 13 | "{{ .CLI.Package }}/verinfo" 14 | ) 15 | 16 | const versionMessage = ` 17 | Version: v%s 18 | Commit: %s 19 | 20 | BuildDate: %s 21 | GoVersion: %s 22 | OS / Arch: %s %s 23 | 24 | {{ with .CLI.Releases }} 25 | Author: {{ .Author }} 26 | Homepage: {{ .Homepage }} 27 | GitHub: {{ .GitHub.URL }} 28 | {{ end }} 29 | ` 30 | 31 | var VersionLong = `Print the build version for {{ .CLI.cliName }}` 32 | 33 | var VersionCmd = &cobra.Command{ 34 | 35 | Use: "version", 36 | 37 | Aliases: []string{ 38 | "ver", 39 | }, 40 | 41 | Short: "print the version", 42 | 43 | Long: VersionLong, 44 | 45 | Run: func(cmd *cobra.Command, args []string) { 46 | {{ if .CLI.ConfigDir }} 47 | s, e := os.UserConfigDir() 48 | fmt.Printf("{{ .CLI.Name }} ConfigDir %q %v\n", filepath.Join(s, "{{ .CLI.ConfigDir }}"), e) 49 | {{ end }} 50 | 51 | fmt.Printf( 52 | versionMessage, 53 | verinfo.Version, 54 | verinfo.Commit, 55 | verinfo.BuildDate, 56 | verinfo.GoVersion, 57 | verinfo.BuildOS, 58 | verinfo.BuildArch, 59 | ) 60 | }, 61 | } 62 | 63 | func init() { 64 | help := VersionCmd.HelpFunc() 65 | usage := VersionCmd.UsageFunc() 66 | 67 | {{ if .CLI.Telemetry }} 68 | thelp := func (cmd *cobra.Command, args []string) { 69 | if VersionCmd.Name() == cmd.Name() { 70 | ga.SendCommandPath("version help") 71 | } 72 | help(cmd, args) 73 | } 74 | tusage := func (cmd *cobra.Command) error { 75 | if VersionCmd.Name() == cmd.Name() { 76 | ga.SendCommandPath("version usage") 77 | } 78 | return usage(cmd) 79 | } 80 | VersionCmd.SetHelpFunc(thelp) 81 | VersionCmd.SetUsageFunc(tusage) 82 | {{ else }} 83 | VersionCmd.SetHelpFunc(help) 84 | VersionCmd.SetUsageFunc(usage) 85 | {{ end }} 86 | } 87 | -------------------------------------------------------------------------------- /templates/cmd_test.go: -------------------------------------------------------------------------------- 1 | {{ if .CMD.Parent }} 2 | package cmd{{ .CMD.Parent.Name }}_test 3 | {{ else }} 4 | package cmd_test 5 | {{ end }} 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/hofstadter-io/hof/script/runtime" 11 | "github.com/hofstadter-io/hof/lib/yagu" 12 | 13 | {{ if .CMD.Commands }} 14 | 15 | {{ if .CMD.Parent.Parent.Parent.Parent }} 16 | "{{ .CLI.Package }}/cmd/{{ .CMD.Parent.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}" 17 | {{ else if .CMD.Parent.Parent.Parent }} 18 | "{{ .CLI.Package }}/cmd/{{ .CMD.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}" 19 | {{ else if .CMD.Parent.Parent }} 20 | "{{ .CLI.Package }}/cmd/{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}" 21 | {{ else if .CMD.Parent }} 22 | "{{ .CLI.Package }}/cmd/{{ .CMD.Parent.Name }}" 23 | {{ else }} 24 | "{{ .CLI.Package }}/cmd" 25 | {{ end }} 26 | 27 | {{ else }} 28 | "{{ .CLI.Package }}/cmd" 29 | {{ end }} 30 | ) 31 | 32 | func TestScript{{ .CMD.CmdName }}CliTests(t *testing.T) { 33 | // setup some directories 34 | {{ if .CMD.Commands }} 35 | {{ if .CMD.Parent.Parent.Parent.Parent }} 36 | dir := "{{ .CMD.Parent.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}/{{ .CMD.cmdName }}" 37 | {{ else if .CMD.Parent.Parent.Parent }} 38 | dir := "{{ .CMD.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}/{{ .CMD.cmdName }}" 39 | {{ else if .CMD.Parent.Parent }} 40 | dir := "{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}/{{ .CMD.cmdName }}" 41 | {{ else if .CMD.Parent }} 42 | dir := "{{ .CMD.Parent.Name }}/{{ .CMD.cmdName }}" 43 | {{ else }} 44 | dir := "{{ .CMD.cmdName }}" 45 | {{ end }} 46 | {{ else }} 47 | dir := "{{ .CMD.cmdName }}" 48 | {{ end }} 49 | workdir := ".workdir/cli/" + dir 50 | yagu.Mkdir(workdir) 51 | 52 | runtime.Run(t, runtime.Params{ 53 | Setup: func (env *runtime.Env) error { 54 | // add any environment variables for your tests here 55 | {{ if .CLI.Telemetry }} 56 | env.Vars = append(env.Vars, "{{ .CLI.CLI_NAME }}_TELEMETRY_DISABLED=1") 57 | {{ end }} 58 | return nil 59 | }, 60 | Funcs: map[string] func (ts* runtime.Script, args[]string) error { 61 | "__{{ .CLI.cliName }}": cmd.CallTS, 62 | }, 63 | Dir: "hls/cli/{{.CMD.cmdName}}", 64 | WorkdirRoot: workdir, 65 | }) 66 | } 67 | 68 | -------------------------------------------------------------------------------- /creators/creator.cue: -------------------------------------------------------------------------------- 1 | package creators 2 | 3 | import ( 4 | "github.com/hofstadter-io/schemas/common" 5 | "github.com/hofstadter-io/schemas/gen" 6 | ) 7 | 8 | Creator: gen.Generator & { 9 | @gen(creator) 10 | 11 | Create: { 12 | Message: { 13 | let name = Input.name 14 | Before: "Creating a new Go Cli" 15 | After: """ 16 | Your new Cli generator is ready, run the following 17 | to generate the code, build the binary, and run \(name). 18 | 19 | now run 'make first' (cd to the --outdir if used) 20 | """ 21 | } 22 | 23 | Args: [...string] 24 | if len(Args) > 0 { 25 | Input: name: Args[0] 26 | } 27 | 28 | Input: { 29 | name: string 30 | repo: string 31 | about: string 32 | releases: bool | *false 33 | updates: bool | *false 34 | telemetry: bool | *false 35 | } 36 | 37 | Prompt: [{ 38 | Name: "name" 39 | Type: "input" 40 | Prompt: "What is your CLI named" 41 | Required: true 42 | Validation: common.NameLabel 43 | }, { 44 | Name: "repo" 45 | Type: "input" 46 | Prompt: "Git repository" 47 | Default: "github.com/user/repo" 48 | Validation: common.NameLabel 49 | }, { 50 | Name: "about" 51 | Type: "input" 52 | Prompt: "Tell us a bit about it..." 53 | Required: true 54 | Validation: common.NameLabel 55 | }, { 56 | Name: "releases" 57 | Type: "confirm" 58 | Prompt: "Enable GoReleaser tooling" 59 | Default: true 60 | }, 61 | 62 | if Input.releases == true { 63 | Name: "updates" 64 | Type: "confirm" 65 | Prompt: "Enable self updating" 66 | Default: true 67 | }, 68 | 69 | if Input.releases == true { 70 | Name: "telemetry" 71 | Type: "confirm" 72 | Prompt: "Enable telemetry" 73 | }, 74 | ] 75 | } 76 | 77 | In: { 78 | Create.Input 79 | ... 80 | } 81 | 82 | Out: [...gen.File] & [ 83 | for file in [ 84 | // "debug.yaml", 85 | "cue.mod/module.cue", 86 | "cli.cue", 87 | "Makefile", 88 | ] {TemplatePath: file, Filepath: file}, 89 | ] 90 | 91 | Templates: [{ 92 | Globs: ["creators/templates/**/*"] 93 | TrimPrefix: "creators/templates/" 94 | }] 95 | Statics: [] 96 | Partials: [] 97 | 98 | EmbeddedTemplates: { 99 | "debug.yaml": { 100 | Content: """ 101 | {{ yaml . }} 102 | """ 103 | } 104 | } 105 | 106 | ModuleName: "" 107 | } 108 | -------------------------------------------------------------------------------- /templates/flags.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "github.com/spf13/pflag" 5 | ) 6 | 7 | var _ *pflag.FlagSet 8 | 9 | {{ if .CMD }} 10 | {{ template "flag-setup" .CMD }} 11 | {{ else }} 12 | {{ template "flag-setup" .CLI }} 13 | {{ end }} 14 | 15 | 16 | {{ define "flag-setup" }} 17 | 18 | {{ $Prefix := "Root" }} 19 | {{ if $.Parent.Parent.Parent.Parent }} 20 | {{ $Prefix = (print $.Parent.Parent.Parent.Parent.Name "__" $.Parent.Parent.Parent.Name "__" $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 21 | {{ else if $.Parent.Parent.Parent }} 22 | {{ $Prefix = (print $.Parent.Parent.Parent.Name "__" $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 23 | {{ else if $.Parent.Parent }} 24 | {{ $Prefix = (print $.Parent.Parent.Name "__" $.Parent.Name "__" $.CmdName) }} 25 | {{ else if $.Parent }} 26 | {{ $Prefix = (print $.Parent.Name "__" $.CmdName) }} 27 | {{ else if $.CmdName }} 28 | {{ $Prefix = $.CmdName }} 29 | {{ end }} 30 | {{ $Prefix = ( title $Prefix ) }} 31 | 32 | {{ if (or $.Flags $.Pflags)}} 33 | var {{ $Prefix }}FlagSet *pflag.FlagSet 34 | 35 | {{ if $.Pflags }} 36 | type {{ $Prefix }}Pflagpole struct { 37 | {{ range $i, $F := $.Pflags }} 38 | {{ $F.FlagName }} {{ $F.Type }} 39 | {{- end }} 40 | } 41 | 42 | func Setup{{ $Prefix }}Pflags(fset *pflag.FlagSet, fpole *{{ $Prefix }}Pflagpole) { 43 | // pflags 44 | {{ range $i, $F := $.Pflags }} 45 | fset.{{- template "cobra-type" $F.Type -}}VarP(&(fpole.{{ $F.FlagName }}), "{{ $F.Long }}", "{{ $F.Short }}", {{ if $F.Default}}{{$F.Default}}{{else}}{{template "go-default" $F.Type }}{{end}}, "{{ $F.Help }}") 46 | {{- end }} 47 | } 48 | 49 | var {{ $Prefix }}Pflags {{ $Prefix }}Pflagpole 50 | {{ end }} 51 | 52 | {{ if $.Flags }} 53 | type {{ $Prefix }}Flagpole struct { 54 | {{ range $i, $F := $.Flags }} 55 | {{ $F.FlagName }} {{ $F.Type }} 56 | {{- end }} 57 | } 58 | 59 | var {{ $Prefix }}Flags {{ $Prefix }}Flagpole 60 | 61 | func Setup{{ $Prefix }}Flags(fset *pflag.FlagSet, fpole *{{ $Prefix }}Flagpole) { 62 | // flags 63 | {{ range $i, $F := $.Flags }} 64 | fset.{{- template "cobra-type" $F.Type -}}VarP(&(fpole.{{ $F.FlagName }}), "{{ $F.Long }}", "{{ $F.Short }}", {{ if $F.Default}}{{$F.Default}}{{else}}{{template "go-default" $F.Type }}{{end}}, "{{ $F.Help }}") 65 | {{- end }} 66 | } 67 | {{ end }} 68 | 69 | func init() { 70 | {{ $Prefix }}FlagSet = pflag.NewFlagSet("{{$Prefix}}", pflag.ContinueOnError) 71 | {{ if $.Pflags }} 72 | Setup{{ $Prefix }}Pflags({{ $Prefix }}FlagSet, &{{ $Prefix }}Pflags) 73 | {{ end }} 74 | {{ if $.Flags }} 75 | Setup{{ $Prefix }}Flags({{ $Prefix }}FlagSet, &{{ $Prefix }}Flags) 76 | {{ end }} 77 | } 78 | 79 | 80 | {{ end }} 81 | {{ end }} 82 | -------------------------------------------------------------------------------- /templates/alt/box-gen.go: -------------------------------------------------------------------------------- 1 | //+build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "go/format" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "text/template" 15 | ) 16 | 17 | const ( 18 | blobFileName string = "blob.go" 19 | embedFolder string = "{% .CLI.EmbedDir %}" 20 | ) 21 | 22 | var conv = map[string]interface{}{"conv": fmtByteSlice} 23 | var tmpl = template.Must(template.New("").Funcs(conv).Parse(`package box 24 | // Code generated by go generate; DO NOT EDIT. 25 | func init() { 26 | {{- range $name, $file := . }} 27 | box.Add("{{ $name }}", []byte{ {{ conv $file }} }) 28 | {{- end }} 29 | }`), 30 | ) 31 | 32 | func fmtByteSlice(s []byte) string { 33 | builder := strings.Builder{} 34 | 35 | for _, v := range s { 36 | builder.WriteString(fmt.Sprintf("%d,", int(v))) 37 | } 38 | 39 | return builder.String() 40 | } 41 | 42 | func main() { 43 | // Checking directory with files 44 | if _, err := os.Stat(embedFolder); os.IsNotExist(err) { 45 | log.Fatal("Static directory does not exists!") 46 | } 47 | 48 | // Create map for filenames 49 | configs := make(map[string][]byte) 50 | 51 | // Walking through embed directory 52 | err := filepath.Walk(embedFolder, func(path string, info os.FileInfo, err error) error { 53 | relativePath := filepath.ToSlash(strings.TrimPrefix(path, embedFolder)) 54 | 55 | if info.IsDir() { 56 | // Skip directories 57 | log.Println(path, "is a directory, skipping...") 58 | return nil 59 | } else { 60 | // If element is a simple file, embed 61 | log.Println(path, "is a file, packing in...") 62 | 63 | b, err := ioutil.ReadFile(path) 64 | if err != nil { 65 | // If file not reading 66 | log.Printf("Error reading %s: %s", path, err) 67 | return err 68 | } 69 | 70 | // Add file name to map 71 | configs[relativePath] = b 72 | } 73 | 74 | return nil 75 | }) 76 | if err != nil { 77 | log.Fatal("Error walking through embed directory:", err) 78 | } 79 | 80 | // Create blob file 81 | f, err := os.Create(blobFileName) 82 | if err != nil { 83 | log.Fatal("Error creating blob file:", err) 84 | } 85 | defer f.Close() 86 | 87 | // Create buffer 88 | builder := &bytes.Buffer{} 89 | 90 | // Execute template 91 | if err = tmpl.Execute(builder, configs); err != nil { 92 | log.Fatal("Error executing template", err) 93 | } 94 | 95 | // Formatting generated code 96 | data, err := format.Source(builder.Bytes()) 97 | if err != nil { 98 | log.Fatal("Error formatting generated code", err) 99 | } 100 | 101 | // Writing blob file 102 | if err = ioutil.WriteFile(blobFileName, data, os.ModePerm); err != nil { 103 | log.Fatal("Error writing blob file", err) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /templates/completions.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | {{ if .CLI.Telemetry }} 9 | "{{ .CLI.Package }}/ga" 10 | {{ end }} 11 | ) 12 | 13 | var ( 14 | CompletionVanillaFlag bool 15 | ) 16 | 17 | func init() { 18 | CompletionCmd.Flags().BoolVarP(&CompletionVanillaFlag, "vanilla", "8", false, "set to only check for an update") 19 | } 20 | 21 | 22 | var CompletionCmd = &cobra.Command{ 23 | Use: "completion", 24 | Aliases: []string{ "completions" }, 25 | Short: "Generate completion helpers for popular terminals", 26 | Long: "Generate completion helpers for popular terminals", 27 | } 28 | 29 | var BashCompletionLong = `Generate Bash completions 30 | 31 | To load completion run 32 | 33 | . <({{ .CLI.cliName }} completion bash) 34 | 35 | To configure your bash shell to load completions for each session add to your bashrc 36 | 37 | # ~/.bashrc or ~/.profile 38 | . <({{ .CLI.cliName }} completion bash) 39 | ` 40 | 41 | var BashCompletionCmd = &cobra.Command{ 42 | Use: "bash", 43 | Short: "Generate Bash completions", 44 | Long: BashCompletionLong, 45 | Run: func(cmd *cobra.Command, args []string) { 46 | RootCmd.GenBashCompletionV2(os.Stdout, false) 47 | }, 48 | } 49 | 50 | var ZshCompletionCmd = &cobra.Command{ 51 | Use: "zsh", 52 | Short: "Generate Zsh completions", 53 | Long: "Generate Zsh completions", 54 | Run: func(cmd *cobra.Command, args []string) { 55 | RootCmd.GenZshCompletion(os.Stdout) 56 | }, 57 | } 58 | 59 | var FishCompletionCmd = &cobra.Command{ 60 | Use: "fish", 61 | Short: "Generate Fish completions", 62 | Long: "Generate Fish completions", 63 | 64 | Run: func(cmd *cobra.Command, args []string) { 65 | RootCmd.GenFishCompletion(os.Stdout, true) 66 | }, 67 | } 68 | 69 | var PowerShellCompletionCmd = &cobra.Command{ 70 | Use: "power-shell", 71 | Aliases: []string{ "windows", "win", "power", "ps" }, 72 | Short: "Generate PowerShell completions", 73 | Long: "Generate PowerShell completions", 74 | 75 | Run: func(cmd *cobra.Command, args []string) { 76 | RootCmd.GenPowerShellCompletion(os.Stdout) 77 | }, 78 | } 79 | 80 | func init() { 81 | CompletionCmd.AddCommand(BashCompletionCmd) 82 | CompletionCmd.AddCommand(ZshCompletionCmd) 83 | CompletionCmd.AddCommand(FishCompletionCmd) 84 | CompletionCmd.AddCommand(PowerShellCompletionCmd) 85 | 86 | help := CompletionCmd.HelpFunc() 87 | usage := CompletionCmd.UsageFunc() 88 | 89 | {{ if .CLI.Telemetry }} 90 | thelp := func (cmd *cobra.Command, args []string) { 91 | if CompletionCmd.Name() == cmd.Name() { 92 | ga.SendCommandPath("completion help") 93 | } 94 | help(cmd, args) 95 | } 96 | tusage := func (cmd *cobra.Command) error { 97 | if CompletionCmd.Name() == cmd.Name() { 98 | ga.SendCommandPath("completion usage") 99 | } 100 | return usage(cmd) 101 | } 102 | CompletionCmd.SetHelpFunc(thelp) 103 | CompletionCmd.SetUsageFunc(tusage) 104 | {{ else }} 105 | CompletionCmd.SetHelpFunc(help) 106 | CompletionCmd.SetUsageFunc(usage) 107 | {{ end }} 108 | } 109 | -------------------------------------------------------------------------------- /templates/alt/goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: "{% .CLI.cliName %}" 2 | 3 | builds: 4 | - binary: "{% .CLI.cliName %}" 5 | main: main.go 6 | 7 | ldflags: 8 | - -s -w 9 | - -X {% .CLI.Package %}/verinfo.Version={{.Version}} 10 | - -X {% .CLI.Package %}/verinfo.Commit={{.FullCommit}} 11 | - -X {% .CLI.Package %}/verinfo.BuildDate={{.Date}} 12 | - -X {% .CLI.Package %}/verinfo.BuildOS={{.Os}} 13 | - -X {% .CLI.Package %}/verinfo.BuildArch={{.Arch}} 14 | - -X {% .CLI.Package %}/verinfo.BuildArm={{.Arm}} 15 | 16 | env: 17 | - CGO_ENABLED=0 18 | 19 | goos: 20 | - darwin 21 | - linux 22 | - windows 23 | goarch: 24 | - amd64 25 | - arm64 26 | 27 | snapshot: 28 | name_template: "{{ .Tag }}-SNAPSHOT-{{.ShortCommit}}" 29 | 30 | archives: 31 | - format: binary 32 | replacements: 33 | darwin: Darwin 34 | linux: Linux 35 | windows: Windows 36 | amd64: x86_64 37 | # Needed hack for binary only uploads 38 | # For more information, check #602 39 | files: 40 | - "thisfiledoesnotexist*" 41 | 42 | 43 | checksum: 44 | name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt' 45 | 46 | changelog: 47 | sort: asc 48 | filters: 49 | exclude: 50 | - '^docs:' 51 | - '^test:' 52 | 53 | 54 | release: 55 | disable: {% .CLI.Releases.Disable %} 56 | draft: {% .CLI.Releases.Draft %} 57 | github: 58 | owner: {% .CLI.Releases.GitHub.Owner %} 59 | name: {% .CLI.Releases.GitHub.Repo %} 60 | 61 | dockers: 62 | - skip_push: {% .CLI.Releases.Disable %} 63 | dockerfile: ../../ci/{% .CLI.cliName %}/docker/Dockerfile.debian 64 | image_templates: 65 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:{{.Tag}}" 66 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }}" 67 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:v{{ .Major }}" 68 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:latest" 69 | 70 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:{{.Tag}}-debian" 71 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }}-debian" 72 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:v{{ .Major }}-debian" 73 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:latest-debian" 74 | 75 | - skip_push: {% .CLI.Releases.Disable %} 76 | dockerfile: ../../ci/{% .CLI.cliName %}/docker/Dockerfile.alpine 77 | image_templates: 78 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:{{.Tag}}-alpine" 79 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }}-alpine" 80 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:v{{ .Major }}-alpine" 81 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:latest-alpine" 82 | 83 | - skip_push: {% .CLI.Releases.Disable %} 84 | dockerfile: ../../ci/{% .CLI.cliName %}/docker/Dockerfile.scratch 85 | image_templates: 86 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:{{.Tag}}-scratch" 87 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }}-scratch" 88 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:v{{ .Major }}-scratch" 89 | - "{% .CLI.Releases.Docker.Repo %}/{{.ProjectName}}:latest-scratch" 90 | -------------------------------------------------------------------------------- /templates/ga.go: -------------------------------------------------------------------------------- 1 | package ga 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/google/uuid" 11 | "github.com/hofstadter-io/cinful" 12 | "github.com/hofstadter-io/hof/lib/yagu" 13 | 14 | "{{ .CLI.Package }}/verinfo" 15 | ) 16 | 17 | var dir, fn, cid string 18 | var isCI bool 19 | 20 | var debug = false 21 | 22 | func init() { 23 | if debug { 24 | fmt.Println("init telemetry") 25 | } 26 | // short-circuit 27 | if os.Getenv("{{ .CLI.CLI_NAME }}_TELEMETRY_DISABLED") != "" { 28 | if debug { 29 | fmt.Println("telemetry disabled in env") 30 | } 31 | cid = "disabled" 32 | return 33 | } 34 | 35 | // check if in CI 36 | vendor := cinful.Info() 37 | if vendor != nil { 38 | if debug { 39 | fmt.Println("in CI") 40 | } 41 | // generate an ID 42 | id, _ := uuid.NewUUID() 43 | cid = id.String() 44 | 45 | isCI = true 46 | cid = "CI-" + cid 47 | return 48 | } 49 | 50 | // setup dir info 51 | ucd, err := os.UserConfigDir() 52 | if err != nil { 53 | cid = "disabled" 54 | return 55 | } 56 | dir = filepath.Join(ucd, "{{ .CLI.TelemetryIdDir }}") 57 | fn = filepath.Join(dir, ".uuid") 58 | 59 | // try reading 60 | cid, err = readGaId() 61 | if err != nil { 62 | cid = "missing" 63 | } 64 | if cid == "disabled" { 65 | if debug { 66 | fmt.Println("telemetry disabled in cfg") 67 | } 68 | return 69 | } 70 | 71 | if debug { 72 | fmt.Println("telemetry ok:", cid) 73 | } 74 | 75 | // does it exist already 76 | if cid != "missing" { 77 | return 78 | } 79 | 80 | // create the ID for the first time 81 | // prompting user for approval 82 | 83 | // if not found, ask and write 84 | approve := askGaID() 85 | if !approve { 86 | err = writeGaId("disabled") 87 | } else { 88 | id, _ := uuid.NewUUID() 89 | cid = id.String() 90 | err = writeGaId(cid) 91 | } 92 | 93 | if err != nil { 94 | fmt.Println("Error writing telemetry config, please let the devs know") 95 | return 96 | } 97 | } 98 | 99 | func SendCommandPath(cmd string) { 100 | if debug { 101 | fmt.Println("try sending:", cmd) 102 | } 103 | cs := strings.Fields(cmd) 104 | c := strings.Join(cs[1:], "/") 105 | l := "user" 106 | if isCI { 107 | l = "ci" 108 | } 109 | SendGaEvent(c, l, 0) 110 | } 111 | 112 | func SendGaEvent(action, label string, value int) { 113 | if os.Getenv("{{ .CLI.CLI_NAME }}_TELEMETRY_DISABLED") != "" { 114 | return 115 | } 116 | 117 | if cid == "disabled" { 118 | return 119 | } 120 | 121 | ua := fmt.Sprintf( 122 | "%s %s (%s/%s)", 123 | "{{ .CLI.cliName }}", verinfo.Version, 124 | verinfo.BuildOS, verinfo.BuildArch, 125 | ) 126 | 127 | cfg := yagu.GaConfig{ 128 | TID: "{{ .CLI.Telemetry }}", 129 | CID: cid, 130 | UA: ua, 131 | CN: "{{ .CLI.cliName }}", 132 | CS: "{{ .CLI.cliName }}/" + verinfo.Version, 133 | CM: verinfo.Version, 134 | } 135 | 136 | evt := yagu.GaEvent{ 137 | Source: cfg.UA, 138 | Category: "{{ .CLI.cliName }}", 139 | Action: action, 140 | Label: label, 141 | } 142 | 143 | if value >= 0 { 144 | evt.Value = value 145 | } 146 | 147 | if debug { 148 | fmt.Printf("sending:\n%#v\n%#v\n", cfg, evt) 149 | } 150 | 151 | yagu.SendGaEvent(cfg, evt) 152 | } 153 | 154 | func readGaId() (string, error) { 155 | // ucd := yagu.UserHomeDir() 156 | 157 | _, err := os.Lstat(fn) 158 | if err != nil { 159 | return "missing", err 160 | } 161 | 162 | content, err := ioutil.ReadFile(fn) 163 | if err != nil { 164 | return "missing", err 165 | } 166 | 167 | if debug { 168 | fmt.Printf("read %q from %s\n", string(content), fn) 169 | } 170 | 171 | return string(content), nil 172 | } 173 | 174 | func writeGaId(value string) error { 175 | 176 | err := os.MkdirAll(dir, 0755) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | err = ioutil.WriteFile(fn, []byte(value), 0644) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | return nil 187 | } 188 | 189 | var askMsg = `{{ .CLI.TelemetryAsk }}` 190 | 191 | func askGaId() bool { 192 | prompt := "\n(y/n) >" 193 | fmt.Printf(askMsg + prompt) 194 | var ans string 195 | fmt.Scanln(&ans) 196 | a := strings.ToLower(ans) 197 | if a == "n" || a == "no" { 198 | return false 199 | } 200 | return true 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hofmod-cli 2 | 3 | A [hof](https://github.com/hofstadter-io/hof) generator for creating advanced Golang CLIs. 4 | 5 | Design your CLI structure, arguments, flags, and a whole host of addons 6 | and then generate the implementation. Changed your mind about what your 7 | CLI should look like? Redesign, regenerate, and keep adding you custom code. 8 | 9 | ### Features: 10 | 11 | - Quickly architect your CLI commands, arguments, flags, configuration 12 | - Built on the fantastic [spf13/cobra](https://github.com/ andspf13/cobra)library for Golang CLIs 13 | - Cross-platform builds and releases using GoReleaser, GitHub, and Docker 14 | - Supports config files in local project falling back to OS specific application dir 15 | - Your CLI will self check for updates and can self install with a user command 16 | - Shell auto completion for bash, fish, zsh, and power shell 17 | - Advanced help system with support for custom overviews, extra topics, and examples 18 | - Telemetry systems which can hook up to Google Analytics 19 | - Golang pprof and many other ENV VARs to control inner behavior 20 | 21 | ### Sites to see: 22 | 23 | - [Schema](./schema) - the design spec your write a CLI in 24 | - [Generator](./gen) - [hof](https://github.com/hofstadter-io/hof) generator definition you invoke 25 | - [Templates](./templates) and [partials](./partials) - files which implement the code 26 | - [Example](https://github.com/hofstadter-io/hof) - the [hof](https://github.com/hofstadter-io/hof) tool leverages and powers this, see the `hof.cue` and `design` directory for relevant files 27 | 28 | ### Usage 29 | 30 | You'll need the [hof](https://github.com/hofstadter-io/hof) tool installed. 31 | You can download `hof` from [the releases page](https://github.com/hofstadter-io/hof/releases). 32 | 33 | Let's start a new project: 34 | 35 | ``` 36 | # Start a project 37 | hof init github.com/verdverm/my-cli 38 | cd my-cli 39 | ``` 40 | 41 | Add the following to the `cue.mods` file (same format as `go.mod`) 42 | 43 | ``` 44 | module github.com/verdverm/my-cli 45 | 46 | cue master 47 | 48 | require ( 49 | github.com/hofstadter-io/hofmod-cli v0.7.0 50 | ) 51 | ``` 52 | 53 | To fetch the module, run: 54 | 55 | ``` 56 | hof mod vendor cue 57 | 58 | # and after the next file, run 59 | hof gen 60 | ``` 61 | 62 | Create a file named `cli.cue` and add the following content: 63 | 64 | ``` 65 | package cli 66 | 67 | import ( 68 | "github.com/hofstadter-io/hofmod-cli/schema" 69 | 70 | "github.com/hofstadter-io/hof/design/cli/cmds" 71 | ) 72 | 73 | # Typically we put the cli under a nested directory 74 | #Outdir: "./cmd/hof" 75 | 76 | #CLI: schema.#Cli & { 77 | # Name and package path (matches outdir) 78 | Name: "hof" 79 | Package: "github.com/hofstadter-io/hof/cmd/hof" 80 | 81 | # Usage and help 82 | Usage: "hof" 83 | Short: "Polyglot Development Tool and Framework" 84 | Long: Short 85 | CustomHelp: #RootCustomHelp 86 | 87 | # Print the help when no subcommands are supplied 88 | OmitRun: true 89 | 90 | # Command stage hooks 91 | PersistentPrerun: true 92 | # You can write code here or... 93 | PersistentPrerunBody: "runtime.Init()" 94 | 95 | PersistentPostrun: true 96 | # ...or add custom code right in the output 97 | 98 | # Persistent flags work for all subcommands too 99 | Pflags: [{ 100 | Name: "labels" 101 | Long: "label" 102 | Short: "l" 103 | Type: "[]string" 104 | Default: "nil" 105 | Help: "Labels for use across all commands" 106 | }, { 107 | Name: "config" 108 | Long: "config" 109 | Short: "" 110 | Type: "string" 111 | Default: "" 112 | Help: "Path to a hof configuration file" 113 | }, ...] 114 | 115 | # Subcommands and nested down as far as you need 116 | Commands: [ 117 | 118 | ] 119 | 120 | // 121 | // Addons 122 | // 123 | Releases: #CliReleases 124 | Updates: true 125 | 126 | ... 127 | } 128 | ``` 129 | 130 | (this was adapted from the [hof](https://github.com/hofstadter-io/hof t) tool) 131 | 132 | Now run `hof gen` to generate your code. 133 | Try adding implementation, and then build: 134 | 135 | ``` 136 | go build -o my-cli cmd/my-cli/main.go 137 | 138 | ./my-cli 139 | ``` 140 | 141 | Update your designs, rerun `hof gen`, rebuild 142 | and keep iterating away! 143 | 144 | -------------------------------------------------------------------------------- /templates/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | {{ if .CLI.EnablePProf }} 7 | "log" 8 | "runtime/pprof" 9 | {{end}} 10 | {{ if .CLI.CustomHelp }} 11 | "strings" 12 | {{ end }} 13 | 14 | "github.com/spf13/cobra" 15 | 16 | {{ if .CLI.Imports }} 17 | {{ range $i, $I := .CLI.Imports }} 18 | {{ $I.As }} "{{ $I.Path }}" 19 | {{ end }} 20 | {{ end }} 21 | 22 | {{/* hack */}} 23 | {{ if .CLI.Flags }} 24 | "{{ .CLI.Package }}/flags" 25 | {{ else if .CLI.Pflags }} 26 | "{{ .CLI.Package }}/flags" 27 | {{ else if .CLI.Topics }} 28 | "{{ .CLI.Package }}/flags" 29 | {{ else if .CLI.Examples }} 30 | "{{ .CLI.Package }}/flags" 31 | {{ else if .CLI.Tutorials }} 32 | "{{ .CLI.Package }}/flags" 33 | {{ end }} 34 | {{ if .CLI.Telemetry }} 35 | "{{ .CLI.Package }}/ga" 36 | {{end}} 37 | ) 38 | 39 | {{ if .CLI.Long }} 40 | var {{ .CLI.Name }}Long = `{{ .CLI.Long }}` 41 | {{ end }} 42 | 43 | {{ template "flag-init" .CLI }} 44 | 45 | {{ if .CLI.PersistentPrerun }} 46 | func RootPersistentPreRun({{- template "lib-args.go" . -}}) (err error) { 47 | {{ if .CLI.PersistentPrerunBody }} 48 | {{ .CLI.PersistentPrerunBody }} 49 | {{ end }} 50 | 51 | return err 52 | } 53 | {{ end }} 54 | 55 | {{ if .CLI.Prerun }} 56 | func RootPreRun({{- template "lib-args.go" . -}}) (err error) { 57 | {{ if .CLI.PrerunBody }} 58 | {{ .CLI.PrerunBody }} 59 | {{ end }} 60 | 61 | return err 62 | } 63 | {{ end }} 64 | 65 | {{ if not .CLI.OmitRun}} 66 | func RootRun({{ template "lib-args.go" . -}}) (err error) { 67 | 68 | {{ if .CLI.Body}} 69 | {{ .CLI.Body}} 70 | {{ end }} 71 | 72 | return err 73 | } 74 | {{ end }} 75 | 76 | {{ if or .CLI.PersistentPostrun .CLI.Updates}} 77 | func RootPersistentPostRun({{- template "lib-args.go" . -}}) (err error) { 78 | 79 | {{ if .CLI.Updates }} 80 | WaitPrintUpdateAvailable() 81 | {{ end }} 82 | 83 | {{ if .CLI.PersistentPostrunBody}} 84 | {{ .CLI.PersistentPostrunBody}} 85 | {{ end }} 86 | 87 | return err 88 | } 89 | {{ end }} 90 | 91 | {{ if .CLI.Postrun}} 92 | func RootPostRun({{- template "lib-args.go" . -}}) (err error) { 93 | 94 | {{ if .CLI.PostrunBody }} 95 | {{ .CLI.PostrunBody }} 96 | {{ end }} 97 | 98 | return err 99 | } 100 | {{ end }} 101 | 102 | var RootCmd = &cobra.Command{ 103 | 104 | {{ if .CLI.Usage}} 105 | Use: "{{ .CLI.Usage }}", 106 | {{ else }} 107 | Use: "{{ .CLI.Name }}", 108 | {{ end }} 109 | 110 | {{ if .CLI.Short}} 111 | Short: "{{ .CLI.Short }}", 112 | {{ end }} 113 | 114 | {{ if .CLI.Long }} 115 | Long: {{ .CLI.Name }}Long, 116 | {{ end }} 117 | 118 | {{ if .CLI.PersistentPrerun }} 119 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 120 | var err error 121 | {{ template "args-parse" .CLI.Args }} 122 | 123 | err = RootPersistentPreRun({{ template "lib-call.go" . }}) 124 | if err != nil { 125 | fmt.Println(err) 126 | os.Exit(1) 127 | } 128 | }, 129 | {{ end }} 130 | 131 | {{ if or .CLI.Prerun .CLI.Telemetry}} 132 | PreRun: func(cmd *cobra.Command, args []string) { 133 | {{ if .CLI.Telemetry }} 134 | ga.SendCommandPath("root") 135 | {{ end }} 136 | 137 | {{ if .CLI.Prerun}} 138 | var err error 139 | {{ template "args-parse" .CLI.Args }} 140 | 141 | err = RootPreRun({{ template "lib-call.go" . }}) 142 | if err != nil { 143 | fmt.Println(err) 144 | os.Exit(1) 145 | } 146 | {{ end }} 147 | }, 148 | {{ end }} 149 | 150 | {{ if not .CLI.OmitRun}} 151 | Run: func(cmd *cobra.Command, args []string) { 152 | var err error 153 | {{ template "args-parse" .CLI.Args }} 154 | 155 | err = RootRun({{ template "lib-call.go" . }}) 156 | if err != nil { 157 | fmt.Println(err) 158 | os.Exit(1) 159 | } 160 | }, 161 | {{ end }} 162 | 163 | {{ if or .CLI.PersistentPostrun .CLI.Updates }} 164 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 165 | var err error 166 | {{ template "args-parse" .CLI.Args }} 167 | 168 | err = RootPersistentPostRun({{ template "lib-call.go" . }}) 169 | if err != nil { 170 | fmt.Println(err) 171 | os.Exit(1) 172 | } 173 | }, 174 | {{ end }} 175 | 176 | {{ if .CLI.Postrun }} 177 | PostRun: func(cmd *cobra.Command, args []string) { 178 | var err error 179 | {{ template "args-parse" .CLI.Args }} 180 | 181 | err = RootPostRun({{ template "lib-call.go" . }}) 182 | if err != nil { 183 | fmt.Println(err) 184 | os.Exit(1) 185 | } 186 | }, 187 | {{ end }} 188 | } 189 | 190 | func RootInit() { 191 | extra := func(cmd *cobra.Command) bool { 192 | {{ if .CLI.Topics }} 193 | if flags.PrintSubject("Topics", " ", flags.RootPflags.Topic, RootTopics) { 194 | return true 195 | } 196 | {{ end }} 197 | 198 | {{ if .CLI.Examples }} 199 | if flags.PrintSubject("Examples", " ", flags.RootPflags.Example, RootExamples) { 200 | return true 201 | } 202 | {{ end }} 203 | 204 | {{ if .CLI.Tutorials }} 205 | if flags.PrintSubject("Tutorials", " ", flags.RootPflags.Tutorial, RootTutorials) { 206 | return true 207 | } 208 | {{ end }} 209 | 210 | return false 211 | } 212 | {{ if .CLI.CustomHelp }} 213 | help := func (cmd *cobra.Command, args []string) { 214 | if extra(cmd) { 215 | return 216 | } 217 | fu := RootCmd.Flags().FlagUsages() 218 | rh := strings.Replace(RootCustomHelp, "<>", fu, 1) 219 | fmt.Println(rh) 220 | } 221 | usage := func(cmd *cobra.Command) error { 222 | if extra(cmd) { 223 | return nil 224 | } 225 | fu := RootCmd.Flags().FlagUsages() 226 | rh := strings.Replace(RootCustomHelp, "<>", fu, 1) 227 | fmt.Println(rh) 228 | return fmt.Errorf("unknown {{ .CLI.cliName }} command") 229 | } 230 | {{ else }} 231 | ohelp := RootCmd.HelpFunc() 232 | ousage := RootCmd.UsageFunc() 233 | help := func (cmd *cobra.Command, args []string) { 234 | if extra(cmd) { 235 | return 236 | } 237 | ohelp(cmd, args) 238 | } 239 | usage := func(cmd *cobra.Command) error { 240 | if extra(cmd) { 241 | return nil 242 | } 243 | return ousage(cmd) 244 | } 245 | {{ end }} 246 | 247 | {{ if .CLI.Telemetry }} 248 | thelp := func (cmd *cobra.Command, args []string) { 249 | if RootCmd.Name() == cmd.Name() { 250 | ga.SendCommandPath("root help") 251 | } 252 | help(cmd, args) 253 | } 254 | tusage := func (cmd *cobra.Command) error { 255 | if RootCmd.Name() == cmd.Name() { 256 | ga.SendCommandPath("root usage") 257 | } 258 | return usage(cmd) 259 | } 260 | RootCmd.SetHelpFunc(thelp) 261 | RootCmd.SetUsageFunc(tusage) 262 | {{ else }} 263 | RootCmd.SetHelpFunc(help) 264 | RootCmd.SetUsageFunc(usage) 265 | {{ end }} 266 | 267 | 268 | {{if .CLI.Updates}} 269 | RootCmd.AddCommand(UpdateCmd) 270 | {{end}} 271 | {{if .CLI.VersionCommand}} 272 | RootCmd.AddCommand(VersionCmd) 273 | {{end}} 274 | {{if .CLI.CompletionCommands}} 275 | RootCmd.AddCommand(CompletionCmd) 276 | {{end}} 277 | 278 | {{if .CLI.Commands}} 279 | {{range $i, $C := .CLI.Commands }} 280 | RootCmd.AddCommand({{ $C.CmdName }}Cmd) 281 | {{- end }} 282 | {{ end }} 283 | } 284 | 285 | func RunExit() { 286 | if err := RunErr(); err != nil { 287 | fmt.Println(err) 288 | os.Exit(1) 289 | } 290 | } 291 | 292 | func RunInt() int { 293 | if err := RunErr(); err != nil { 294 | fmt.Println(err) 295 | return 1 296 | } 297 | return 0 298 | } 299 | 300 | func RunErr() error { 301 | {{ if .CLI.EnablePProf }} 302 | if fn := os.Getenv("{{.CLI.CLI_NAME}}_CPU_PROFILE"); fn != "" { 303 | f, err := os.Create(fn) 304 | if err != nil { 305 | log.Fatal("Could not create file for CPU profile:", err) 306 | } 307 | defer f.Close() 308 | 309 | err = pprof.StartCPUProfile(f) 310 | if err != nil { 311 | log.Fatal("Could not start CPU profile process:", err) 312 | } 313 | 314 | defer pprof.StopCPUProfile() 315 | } 316 | {{ end }} 317 | 318 | RootInit() 319 | return RootCmd.Execute() 320 | } 321 | 322 | {{ if .CLI.CustomHelp }} 323 | const RootCustomHelp = `{{ .CLI.CustomHelp }}` 324 | {{ end }} 325 | 326 | {{ if .CLI.Topics }} 327 | var RootTopics = map[string]string { 328 | {{- range $k, $v := .CLI.Topics }} 329 | "{{ $k }}": `{{ replace $v "`" "¡" -1 }}`, 330 | {{- end}} 331 | } 332 | {{ end }} 333 | 334 | {{ if .CLI.Examples }} 335 | var RootExamples = map[string]string { 336 | {{- range $k, $v := .CLI.Examples }} 337 | "{{ $k }}": `{{ replace $v "`" "¡" -1 }}`, 338 | {{- end}} 339 | } 340 | {{ end }} 341 | 342 | {{ if .CLI.Tutorials }} 343 | var RootTutorials = map[string]string { 344 | {{- range $k, $v := .CLI.Tutorials }} 345 | "{{ $k }}": `{{ replace $v "`" "¡" -1 }}`, 346 | {{- end}} 347 | } 348 | {{ end }} 349 | 350 | -------------------------------------------------------------------------------- /gen/cli.cue: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "list" 5 | 6 | hof "github.com/hofstadter-io/schemas/gen" 7 | 8 | "github.com/hofstadter-io/hofmod-cli/schema" 9 | "github.com/hofstadter-io/hofmod-cli/templates" 10 | ) 11 | 12 | Generator: hof.Generator & { 13 | Cli: schema.Cli 14 | Outdir?: string | *"./" 15 | 16 | OutdirConfig: { 17 | CiOutdir: string | *"ci/\(In.CLI.cliName)" 18 | CliOutdir: string | *"cmd/\(In.CLI.cliName)" 19 | CmdOutdir: string | *"cmd/\(In.CLI.cliName)/cmd" 20 | FlagsOutdir: string | *"cmd/\(In.CLI.cliName)/flags" 21 | } 22 | 23 | // Internal 24 | In: { 25 | CLI: Cli 26 | } 27 | 28 | basedir: "cmd/\(In.CLI.cliName)" 29 | 30 | ModuleName: string | *"github.com/hofstadter-io/hofmod-cli" 31 | 32 | Templates: [{ 33 | Globs: ["templates/*"] 34 | TrimPrefix: "templates/" 35 | }, { 36 | Globs: ["templates/alt/*"] 37 | TrimPrefix: "templates/" 38 | Delims: { 39 | LHS: "{%" 40 | RHS: "%}" 41 | } 42 | }] 43 | 44 | Statics: [] 45 | 46 | // Combine everything together and output files that might need to be generated 47 | All: [ 48 | for _, F in OnceFiles {F}, 49 | for _, F in S1_Cmds {F}, 50 | for _, F in S2_Cmds {F}, 51 | for _, F in S3_Cmds {F}, 52 | for _, F in S4_Cmds {F}, 53 | for _, F in S5_Cmds {F}, 54 | for _, F in S1_Flags {F}, 55 | for _, F in S2_Flags {F}, 56 | for _, F in S3_Flags {F}, 57 | for _, F in S4_Flags {F}, 58 | for _, F in S5_Flags {F}, 59 | ] 60 | 61 | Out: [...hof.File] & All 62 | 63 | // Files that are not repeatedly used, they are generated once for the whole CLI 64 | OnceFiles: [...hof.File] & [ 65 | { 66 | TemplatePath: "go.mod" 67 | Filepath: "go.mod" 68 | }, { 69 | TemplatePath: "Makefile" 70 | Filepath: "Makefile" 71 | }, { 72 | TemplatePath: "main.go" 73 | Filepath: "\(OutdirConfig.CliOutdir)/main.go" 74 | }, { 75 | TemplatePath: "root.go" 76 | Filepath: "\(OutdirConfig.CmdOutdir)/root.go" 77 | }, { 78 | TemplatePath: "flags.go" 79 | Filepath: "\(OutdirConfig.FlagsOutdir)/root.go" 80 | }, { 81 | if (In.CLI.VersionCommand & true) != _|_ { 82 | TemplatePath: "version.go" 83 | Filepath: "\(OutdirConfig.CmdOutdir)/version.go" 84 | } 85 | }, { 86 | if (In.CLI.VersionCommand & true) != _|_ { 87 | TemplatePath: "verinfo.go" 88 | Filepath: "\(OutdirConfig.CliOutdir)/verinfo/verinfo.go" 89 | } 90 | }, { 91 | if (In.CLI.Updates & true) != _|_ { 92 | TemplatePath: "update.go" 93 | Filepath: "\(OutdirConfig.CmdOutdir)/update.go" 94 | } 95 | }, { 96 | if (In.CLI.CompletionCommands & true) != _|_ { 97 | TemplatePath: "completions.go" 98 | Filepath: "\(OutdirConfig.CmdOutdir)/completions.go" 99 | } 100 | }, { 101 | if In.CLI.Telemetry != _|_ { 102 | TemplatePath: "ga.go" 103 | Filepath: "\(OutdirConfig.CliOutdir)/ga/ga.go" 104 | } 105 | }, { 106 | if In.CLI.Releases != _|_ { 107 | TemplateContent: templates.DockerfileDebian 108 | Filepath: "\(OutdirConfig.CiOutdir)/docker/Dockerfile.debian" 109 | } 110 | }, { 111 | if In.CLI.Releases != _|_ { 112 | TemplateContent: templates.DockerfileAlpine 113 | Filepath: "\(OutdirConfig.CiOutdir)/docker/Dockerfile.alpine" 114 | } 115 | }, { 116 | if In.CLI.Releases != _|_ { 117 | TemplateContent: templates.DockerfileScratch 118 | Filepath: "\(OutdirConfig.CiOutdir)/docker/Dockerfile.scratch" 119 | } 120 | }, { 121 | if In.CLI.Releases != _|_ { 122 | TemplatePath: "alt/goreleaser.yml" 123 | Filepath: "\(OutdirConfig.CliOutdir)/.goreleaser.yml" 124 | } 125 | }, 126 | 127 | ] 128 | 129 | // Sub command tree 130 | S1_Cmds: [...hof.File] & [ 131 | for _, C in Cli.Commands { 132 | In: { 133 | CMD: { 134 | C 135 | PackageName: "cmd" 136 | } 137 | } 138 | TemplatePath: "cmd.go" 139 | Filepath: "\(OutdirConfig.CmdOutdir)/\(In.CMD.Name).go" 140 | }, 141 | ] 142 | 143 | S2C: list.FlattenN([ for P in S1_Cmds if len(P.In.CMD.Commands) > 0 { 144 | [ for C in P.In.CMD.Commands {C, Parent: {Name: P.In.CMD.Name}}] 145 | }], 1) 146 | S2_Cmds: [...hof.File] & [ // List comprehension 147 | for _, C in S2C { 148 | In: { 149 | CMD: C 150 | } 151 | TemplatePath: "cmd.go" 152 | Filepath: "\(OutdirConfig.CmdOutdir)/\(C.Parent.Name)/\(C.Name).go" 153 | }, 154 | ] 155 | 156 | S3C: list.FlattenN([ for P in S2_Cmds if len(P.In.CMD.Commands) > 0 { 157 | [ for C in P.In.CMD.Commands {C, Parent: {Name: P.In.CMD.Name, Parent: P.In.CMD.Parent}}] 158 | }], 1) 159 | S3_Cmds: [...hof.File] & [ // List comprehension 160 | for _, C in S3C { 161 | In: { 162 | CMD: C 163 | } 164 | TemplatePath: "cmd.go" 165 | Filepath: "\(OutdirConfig.CmdOutdir)/\(C.Parent.Parent.Name)/\(C.Parent.Name)/\(C.Name).go" 166 | }, 167 | ] 168 | 169 | S4C: list.FlattenN([ for P in S3_Cmds if len(P.In.CMD.Commands) > 0 { 170 | [ for C in P.In.CMD.Commands {C, Parent: {Name: P.In.CMD.Name, Parent: P.In.CMD.Parent}}] 171 | }], 1) 172 | S4_Cmds: [...hof.File] & [ // List comprehension 173 | for _, C in S4C { 174 | In: { 175 | CMD: C 176 | } 177 | TemplatePath: "cmd.go" 178 | Filepath: "\(OutdirConfig.CmdOutdir)/\(C.Parent.Parent.Parent.Name)/\(C.Parent.Parent.Name)/\(C.Parent.Name)/\(C.Name).go" 179 | }, 180 | ] 181 | 182 | S5C: list.FlattenN([ for P in S4_Cmds if len(P.In.CMD.Commands) > 0 { 183 | [ for C in P.In.CMD.Commands {C, Parent: {Name: P.In.CMD.Name, Parent: P.In.CMD.Parent}}] 184 | }], 1) 185 | S5_Cmds: [...hof.File] & [ // List comprehension 186 | for _, C in S5C { 187 | In: { 188 | CMD: C 189 | } 190 | TemplatePath: "cmd.go" 191 | Filepath: "\(OutdirConfig.CmdOutdir)/\(C.Parent.Parent.Parent.Parent.Name)/\(C.Parent.Parent.Parent.Name)/\(C.Parent.Parent.Name)/\(C.Parent.Name)/\(C.Name).go" 192 | }, 193 | ] 194 | 195 | // Persistent Flags 196 | S1_Flags: [...hof.File] & [ // List comprehension 197 | for _, C in Cli.Commands if C.Pflags != _|_ || C.Flags != _|_ { 198 | In: { 199 | // CLI 200 | CMD: { 201 | C 202 | PackageName: "flags" 203 | } 204 | } 205 | TemplatePath: "flags.go" 206 | Filepath: "\(OutdirConfig.FlagsOutdir)/\(In.CMD.Name).go" 207 | }, 208 | ] 209 | 210 | S2F: list.FlattenN([ for P in S1_Cmds if len(P.In.CMD.Commands) > 0 { 211 | [ for C in P.In.CMD.Commands if C.Pflags != _|_ || C.Flags != _|_ {C, Parent: {Name: P.In.CMD.Name}}] 212 | }], 1) 213 | S2_Flags: [...hof.File] & [ // List comprehension 214 | for _, C in S2F { 215 | In: { 216 | CMD: { 217 | C 218 | PackageName: "flags" 219 | } 220 | } 221 | TemplatePath: "flags.go" 222 | Filepath: "\(OutdirConfig.FlagsOutdir)/\(C.Parent.Name)__\(C.Name).go" 223 | }, 224 | ] 225 | 226 | S3F: list.FlattenN([ for P in S2_Cmds if len(P.In.CMD.Commands) > 0 { 227 | [ for C in P.In.CMD.Commands if C.Pflags != _|_ || C.Flags != _|_ {C, Parent: {Name: P.In.CMD.Name, Parent: P.In.CMD.Parent}}] 228 | }], 1) 229 | S3_Flags: [...hof.File] & [ // List comprehension 230 | for _, C in S3F { 231 | In: { 232 | CMD: { 233 | C 234 | PackageName: "flags" 235 | } 236 | } 237 | TemplatePath: "flags.go" 238 | Filepath: "\(OutdirConfig.FlagsOutdir)/\(C.Parent.Parent.Name)__\(C.Parent.Name)__\(C.Name).go" 239 | }, 240 | ] 241 | 242 | S4F: list.FlattenN([ for P in S3_Cmds if len(P.In.CMD.Commands) > 0 { 243 | [ for C in P.In.CMD.Commands if C.Pflags != _|_ || C.Flags != _|_ {C, Parent: {Name: P.In.CMD.Name, Parent: P.In.CMD.Parent}}] 244 | }], 1) 245 | S4_Flags: [...hof.File] & [ // List comprehension 246 | for _, C in S4F { 247 | In: { 248 | CMD: { 249 | C 250 | PackageName: "flags" 251 | } 252 | } 253 | TemplatePath: "flags.go" 254 | Filepath: "\(OutdirConfig.FlagsOutdir)/\(C.Parent.Parent.Parent.Name)__\(C.Parent.Parent.Name)__\(C.Parent.Name)__\(C.Name).go" 255 | }, 256 | ] 257 | 258 | S5F: list.FlattenN([ for P in S4_Cmds if len(P.In.CMD.Commands) > 0 { 259 | [ for C in P.In.CMD.Commands if C.Pflags != _|_ || C.Flags != _|_ {C, Parent: {Name: P.In.CMD.Name, Parent: P.In.CMD.Parent}}] 260 | }], 1) 261 | S5_Flags: [...hof.File] & [ // List comprehension 262 | for _, C in S5F { 263 | In: { 264 | CMD: { 265 | C 266 | PackageName: "flags" 267 | } 268 | } 269 | TemplatePath: "flags.go" 270 | Filepath: "\(OutdirConfig.FlagsOutdir)/\(C.Parent.Parent.Parent.Parent.Name)__\(C.Parent.Parent.Parent.Name)__\(C.Parent.Parent.Name)__\(C.Parent.Name)__\(C.Name).go" 271 | }, 272 | ] 273 | 274 | ... 275 | } 276 | 277 | // backwards compat 278 | #HofGenerator: Generator 279 | #Generator: Generator 280 | -------------------------------------------------------------------------------- /templates/cmd.go: -------------------------------------------------------------------------------- 1 | {{ if .CMD.Parent }} 2 | package cmd{{ .CMD.Parent.Name }} 3 | {{ else }} 4 | package cmd 5 | {{ end }} 6 | 7 | import ( 8 | {{ if .CMD.CustomHelp }} 9 | {{ if not .CMD.HasAnyRun }} 10 | "fmt" 11 | {{end}} 12 | {{end}} 13 | {{ if .CMD.HasAnyRun }} 14 | "fmt" 15 | "os" 16 | {{end}} 17 | {{- if (and (len .CMD.Commands) (not .CMD.OmitRun)) }} 18 | "path/filepath" 19 | {{ end }} 20 | 21 | "github.com/spf13/cobra" 22 | 23 | {{ if .CMD.Imports }} 24 | {{ range $i, $I := .CMD.Imports }} 25 | {{ $I.As }} "{{ $I.Path }}" 26 | {{ end }} 27 | {{ end }} 28 | 29 | {{ if .CMD.Commands }} 30 | {{ if .CMD.Parent.Parent.Parent.Parent }} 31 | "{{ .CLI.Package }}/cmd/{{ .CMD.Parent.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}/{{ .CMD.cmdName }}" 32 | {{ else if .CMD.Parent.Parent.Parent }} 33 | "{{ .CLI.Package }}/cmd/{{ .CMD.Parent.Parent.Parent.Name }}/{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}/{{ .CMD.cmdName }}" 34 | {{ else if .CMD.Parent.Parent }} 35 | "{{ .CLI.Package }}/cmd/{{ .CMD.Parent.Parent.Name }}/{{ .CMD.Parent.Name }}/{{ .CMD.cmdName }}" 36 | {{ else if .CMD.Parent }} 37 | "{{ .CLI.Package }}/cmd/{{ .CMD.Parent.Name }}/{{ .CMD.cmdName }}" 38 | {{ else }} 39 | "{{ .CLI.Package }}/cmd/{{ .CMD.cmdName }}" 40 | {{ end }} 41 | {{ end }} 42 | 43 | {{/* hack */}} 44 | {{ if .CMD.Flags }} 45 | "{{ .CLI.Package }}/flags" 46 | {{ else if .CMD.Pflags }} 47 | "{{ .CLI.Package }}/flags" 48 | {{ else if .CMD.Topics }} 49 | "{{ .CLI.Package }}/flags" 50 | {{ else if .CMD.Examples }} 51 | "{{ .CLI.Package }}/flags" 52 | {{ else if .CMD.Tutorials }} 53 | "{{ .CLI.Package }}/flags" 54 | {{ end }} 55 | {{ if .CLI.Telemetry }} 56 | "{{ .CLI.Package }}/ga" 57 | {{end}} 58 | ) 59 | 60 | {{ if .CMD.Long }} 61 | var {{ .CMD.Name }}Long = `{{ .CMD.Long }}` 62 | {{ end }} 63 | 64 | {{ template "flag-init" .CMD }} 65 | 66 | {{ if .CMD.PersistentPrerun }} 67 | func {{ .CMD.CmdName }}PersistentPreRun({{- template "lib-args.go" . -}}) (err error) { 68 | {{ if .CMD.PersistentPrerunBody }} 69 | {{ .CMD.PersistentPrerunBody }} 70 | {{ end }} 71 | 72 | return err 73 | } 74 | {{ end }} 75 | 76 | {{ if .CMD.Prerun}} 77 | func {{ .CMD.CmdName }}PreRun({{- template "lib-args.go" . -}}) (err error) { 78 | {{ if .CMD.PrerunBody }} 79 | {{ .CMD.PrerunBody }} 80 | {{ end }} 81 | 82 | return err 83 | } 84 | {{ end }} 85 | 86 | {{ if not .CMD.OmitRun}} 87 | func {{ .CMD.CmdName }}Run({{ template "lib-args.go" . -}}) (err error) { 88 | {{ if .CMD.Body}} 89 | {{ .CMD.Body}} 90 | {{ else }} 91 | // you can safely comment this print out 92 | fmt.Println("not implemented") 93 | {{ end }} 94 | 95 | return err 96 | } 97 | {{ end }} 98 | 99 | {{ if .CMD.PersistentPostrun}} 100 | func {{ .CMD.CmdName }}PersistentPostRun({{- template "lib-args.go" . -}}) (err error) { 101 | 102 | {{ if .CMD.PersistentPostrunBody}} 103 | {{ .CMD.PersistentPostrunBody}} 104 | {{ end }} 105 | 106 | return err 107 | } 108 | {{ end }} 109 | 110 | {{ if .CMD.Postrun}} 111 | func {{ .CMD.CmdName }}PostRun({{- template "lib-args.go" . -}}) (err error) { 112 | 113 | {{ if .CMD.PostrunBody }} 114 | {{ .CMD.PostrunBody }} 115 | {{ end }} 116 | 117 | return err 118 | } 119 | {{ end }} 120 | var {{ .CMD.CmdName }}Cmd = &cobra.Command{ 121 | 122 | {{ if .CMD.Usage}} 123 | Use: "{{ .CMD.Usage }}", 124 | {{ else }} 125 | Use: "{{ .CMD.Name }}", 126 | {{ end }} 127 | 128 | {{ if .CMD.Hidden }} 129 | Hidden: true, 130 | {{ end }} 131 | 132 | {{ if .CMD.Aliases }} 133 | Aliases: []string{ 134 | {{range $i, $AL := .CMD.Aliases}}"{{$AL}}", 135 | {{end}} 136 | }, 137 | {{ end }} 138 | 139 | {{ if .CMD.Short}} 140 | Short: "{{ .CMD.Short }}", 141 | {{ end }} 142 | 143 | {{ if .CMD.Long }} 144 | Long: {{ .CMD.Name }}Long, 145 | {{ end }} 146 | 147 | {{ if (and (len .CMD.Commands) (not .CMD.OmitRun)) }} 148 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 149 | glob := toComplete + "*" 150 | matches, _ := filepath.Glob(glob) 151 | return matches, cobra.ShellCompDirectiveDefault 152 | }, 153 | 154 | {{ end }} 155 | 156 | {{ if .CMD.PersistentPrerun }} 157 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 158 | var err error 159 | {{ template "args-parse" .CMD.Args }} 160 | 161 | err = {{ .CMD.CmdName }}PersistentPreRun({{ template "lib-call.go" . }}) 162 | if err != nil { 163 | fmt.Println(err) 164 | os.Exit(1) 165 | } 166 | }, 167 | {{ end }} 168 | 169 | {{ if .CMD.Prerun }} 170 | PreRun: func(cmd *cobra.Command, args []string) { 171 | 172 | {{ if .CMD.Prerun}} 173 | var err error 174 | {{ template "args-parse" .CMD.Args }} 175 | 176 | err = {{ .CMD.CmdName }}PreRun({{ template "lib-call.go" . }}) 177 | if err != nil { 178 | fmt.Println(err) 179 | os.Exit(1) 180 | } 181 | {{ end }} 182 | }, 183 | {{ end }} 184 | 185 | {{ if not .CMD.OmitRun}} 186 | Run: func(cmd *cobra.Command, args []string) { 187 | {{ if .CLI.Telemetry }} 188 | ga.SendCommandPath(cmd.CommandPath()) 189 | {{ end }} 190 | 191 | var err error 192 | {{ template "args-parse" .CMD.Args }} 193 | 194 | err = {{ .CMD.CmdName }}Run({{ template "lib-call.go" . }}) 195 | if err != nil { 196 | fmt.Println(err) 197 | os.Exit(1) 198 | } 199 | }, 200 | {{ end }} 201 | 202 | {{ if .CMD.PersistentPostrun}} 203 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 204 | var err error 205 | {{ template "args-parse" .CMD.Args }} 206 | 207 | err = {{ .CMD.CmdName }}PersistentPostRun({{ template "lib-call.go" . }}) 208 | if err != nil { 209 | fmt.Println(err) 210 | os.Exit(1) 211 | } 212 | }, 213 | {{ end }} 214 | 215 | {{ if .CMD.Postrun}} 216 | PostRun: func(cmd *cobra.Command, args []string) { 217 | var err error 218 | {{ template "args-parse" .CMD.Args }} 219 | 220 | err = {{ .CMD.CmdName }}PostRun({{ template "lib-call.go" . }}) 221 | if err != nil { 222 | fmt.Println(err) 223 | os.Exit(1) 224 | } 225 | }, 226 | {{ end }} 227 | } 228 | 229 | func init() { 230 | extra := func(cmd *cobra.Command) bool { 231 | {{ if .CMD.Topics }} 232 | if flags.PrintSubject("Topics\n", "", flags.RootPflags.Topic, {{ .CMD.CmdName }}Topics) { 233 | return true 234 | } 235 | {{ end }} 236 | 237 | {{ if .CMD.Examples }} 238 | if flags.PrintSubject("Examples\n", "", flags.RootPflags.Example, {{ .CMD.CmdName }}Examples) { 239 | return true 240 | } 241 | {{ end }} 242 | 243 | {{ if .CMD.Tutorials }} 244 | if flags.PrintSubject("Tutorials\n", "", flags.RootPflags.Tutorial, {{ .CMD.CmdName }}Tutorials) { 245 | return true 246 | } 247 | {{ end }} 248 | 249 | return false 250 | } 251 | {{ if .CMD.CustomHelp }} 252 | help := func (cmd *cobra.Command, args []string) { 253 | {{ if .CLI.Telemetry }} 254 | ga.SendCommandPath(cmd.CommandPath() + " help") 255 | {{ end }} 256 | if extra(cmd) { 257 | return 258 | } 259 | fu := {{ $.CMD.CmdName }}Cmd.Flags().FlagUsages() 260 | ch := strings.Replace({{ $.CMD.CmdName }}CustomHelp, "<>", fu, 1) 261 | fmt.Println(ch) 262 | {{ if .CMD.TBDLong }} 263 | fmt.Println("\nstatus: {{ .CMD.TBDLong }}") 264 | {{ end }} 265 | } 266 | usage := func (cmd *cobra.Command) error { 267 | if extra(cmd) { 268 | return nil 269 | } 270 | fu := {{ $.CMD.CmdName }}Cmd.Flags().FlagUsages() 271 | ch := strings.Replace({{ $.CMD.CmdName }}CustomHelp, "<>", fu, 1) 272 | fmt.Println(ch) 273 | {{ if .CMD.TBDLong }} 274 | fmt.Println("\nstatus: {{ .CMD.TBDLong }}") 275 | {{ end }} 276 | return fmt.Errorf("unknown command %q", cmd.Name()) 277 | } 278 | {{ else }} 279 | ohelp := {{ $.CMD.CmdName }}Cmd.HelpFunc() 280 | ousage := {{ $.CMD.CmdName }}Cmd.UsageFunc() 281 | 282 | help := func (cmd *cobra.Command, args []string) { 283 | {{ if .CLI.Telemetry }} 284 | ga.SendCommandPath(cmd.CommandPath() + " help") 285 | {{ end }} 286 | if extra(cmd) { 287 | return 288 | } 289 | ohelp(cmd, args) 290 | } 291 | usage := func(cmd *cobra.Command) error { 292 | if extra(cmd) { 293 | return nil 294 | } 295 | return ousage(cmd) 296 | } 297 | {{ end }} 298 | 299 | thelp := func (cmd *cobra.Command, args []string) { 300 | help(cmd, args) 301 | } 302 | tusage := func (cmd *cobra.Command) error { 303 | return usage(cmd) 304 | } 305 | {{ $.CMD.CmdName }}Cmd.SetHelpFunc(thelp) 306 | {{ $.CMD.CmdName }}Cmd.SetUsageFunc(tusage) 307 | 308 | {{if .CMD.Commands}} 309 | {{- range $i, $C := .CMD.Commands }} 310 | {{ $.CMD.CmdName }}Cmd.AddCommand(cmd{{ $.CMD.cmdName }}.{{ $C.CmdName }}Cmd) 311 | {{- end}} 312 | {{ end }} 313 | } 314 | 315 | {{ if .CMD.CustomHelp }} 316 | const {{ $.CMD.CmdName }}CustomHelp = `{{ .CMD.CustomHelp }}` 317 | {{ end }} 318 | 319 | {{ if .CMD.Topics }} 320 | var {{ .CMD.CmdName }}Topics = map[string]string { 321 | {{- range $k, $v := .CMD.Topics }} 322 | "{{ $k }}": `{{ replace $v "`" "¡" -1 }}`, 323 | {{- end}} 324 | } 325 | {{ end }} 326 | 327 | {{ if .CMD.Examples }} 328 | var {{ .CMD.CmdName }}Examples = map[string]string { 329 | {{- range $k, $v := .CMD.Examples }} 330 | "{{ $k }}": `{{ replace $v "`" "¡" -1 }}`, 331 | {{- end}} 332 | } 333 | {{ end }} 334 | 335 | {{ if .CMD.Tutorials }} 336 | var {{ .CMD.CmdName }}Tutorials = map[string]string { 337 | {{- range $k, $v := .CMD.Tutorials }} 338 | "{{ $k }}": `{{ replace $v "`" "¡" -1 }}`, 339 | {{- end}} 340 | } 341 | {{ end }} 342 | -------------------------------------------------------------------------------- /templates/update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | "time" 13 | 14 | "github.com/spf13/cobra" 15 | "github.com/parnurzeal/gorequest" 16 | 17 | {{ if .CLI.Telemetry }} 18 | "{{ .CLI.Package }}/ga" 19 | {{ end }} 20 | "{{ .CLI.Package }}/verinfo" 21 | ) 22 | 23 | var UpdateLong = `Print the build version for {{ .CLI.cliName }}` 24 | 25 | var ( 26 | UpdateCheckFlag bool 27 | UpdateVersionFlag string 28 | 29 | UpdateStarted bool 30 | UpdateErrored bool 31 | UpdateChecked bool 32 | UpdateAvailable *ProgramVersion 33 | UpdateData []interface{} 34 | ) 35 | 36 | func init() { 37 | UpdateCmd.Flags().BoolVarP(&UpdateCheckFlag, "check", "", false, "only check for an update") 38 | UpdateCmd.Flags().BoolVarP(&UpdateCheckFlag, "list", "", false, "show local installed version") 39 | UpdateCmd.Flags().StringVarP(&UpdateVersionFlag, "version", "V", "", "the version to update or install") 40 | } 41 | 42 | const updateMessage = ` 43 | Updates available. v%s -> %s 44 | 45 | run '{{ .CLI.cliName }} update' to get the latest. 46 | 47 | ` 48 | 49 | // TODO, add a curl to the above? or os specific? 50 | 51 | var UpdateCmd = &cobra.Command{ 52 | 53 | Use: "update", 54 | 55 | Short: "update the {{ .CLI.cliName }} tool", 56 | 57 | Long: UpdateLong, 58 | 59 | PreRun: func(cmd *cobra.Command, args []string) { 60 | {{ if .CLI.Telemetry }} 61 | ga.SendCommandPath("update") 62 | {{ end }} 63 | 64 | }, 65 | 66 | Run: func(cmd *cobra.Command, args []string) { 67 | 68 | latest, err := CheckUpdate(true) 69 | if err != nil { 70 | fmt.Println(err) 71 | os.Exit(-1) 72 | } 73 | 74 | // Semver Check? 75 | cur := ProgramVersion{Version: "v" + verinfo.Version} 76 | if latest.Version == cur.Version || ( UpdateVersionFlag == "" && cur.Version == "vLocal" ) { 77 | return 78 | } else { 79 | if UpdateCheckFlag { 80 | PrintUpdateAvailable() 81 | return 82 | } 83 | } 84 | 85 | err = InstallUpdate() 86 | if err != nil { 87 | fmt.Println(err) 88 | os.Exit(-1) 89 | } 90 | }, 91 | } 92 | 93 | func init() { 94 | go CheckUpdate(false) 95 | 96 | help := UpdateCmd.HelpFunc() 97 | usage := UpdateCmd.UsageFunc() 98 | 99 | {{ if .CLI.Telemetry }} 100 | thelp := func (cmd *cobra.Command, args []string) { 101 | if UpdateCmd.Name() == cmd.Name() { 102 | ga.SendCommandPath("update help") 103 | } 104 | help(cmd, args) 105 | } 106 | tusage := func (cmd *cobra.Command) error { 107 | if UpdateCmd.Name() == cmd.Name() { 108 | ga.SendCommandPath("update usage") 109 | } 110 | return usage(cmd) 111 | } 112 | UpdateCmd.SetHelpFunc(thelp) 113 | UpdateCmd.SetUsageFunc(tusage) 114 | {{ else }} 115 | UpdateCmd.SetHelpFunc(help) 116 | UpdateCmd.SetUsageFunc(usage) 117 | {{ end }} 118 | } 119 | 120 | type ProgramVersion struct { 121 | Version string 122 | URL string 123 | } 124 | 125 | func CheckUpdate(manual bool) (ver ProgramVersion, err error) { 126 | if !manual && os.Getenv("{{ .CLI.CLI_NAME }}_UPDATES_DISABLED") != "" { 127 | return 128 | } 129 | UpdateStarted = true 130 | cur := ProgramVersion{Version: "v" + verinfo.Version} 131 | 132 | checkURL := "{{ .CLI.Releases.GitHub.Rel }}/latest" 133 | if UpdateVersionFlag != "" { 134 | checkURL = "{{ .CLI.Releases.GitHub.Rel }}/tags/" + UpdateVersionFlag 135 | manual = true 136 | } 137 | 138 | req := gorequest.New() 139 | if os.Getenv("GITHUB_TOKEN") != "" { 140 | req = req.SetBasicAuth("github-token", os.Getenv("GITHUB_TOKEN")) 141 | } 142 | resp, b, errs := req.Get(checkURL).EndBytes() 143 | UpdateErrored = true 144 | 145 | check := "http2: server sent GOAWAY and closed the connection" 146 | if len(errs) != 0 && !strings.Contains(errs[0].Error(), check) { 147 | // fmt.Println("errs:", errs) 148 | return ver, errs[0] 149 | } 150 | 151 | if len(errs) != 0 || resp.StatusCode >= 500 { 152 | return ver, fmt.Errorf("Internal Error: " + string(b)) 153 | } 154 | if resp.StatusCode >= 400 { 155 | if resp.StatusCode == 404 { 156 | fmt.Println("404?!", checkURL) 157 | return ver, fmt.Errorf("No releases available :[") 158 | } 159 | return ver, fmt.Errorf("Bad Request: " + string(b)) 160 | } 161 | 162 | UpdateErrored = false 163 | // fmt.Println(string(b)) 164 | 165 | var gh map[string]interface{} 166 | err = json.Unmarshal(b, &gh) 167 | if err != nil { 168 | return ver, err 169 | } 170 | 171 | nameI, ok := gh["name"] 172 | if !ok { 173 | return ver, fmt.Errorf("Internal Error: could not find version in update check response") 174 | } 175 | name, ok := nameI.(string) 176 | if !ok { 177 | return ver, fmt.Errorf("Internal Error: version is not a string in update check response") 178 | } 179 | ver.Version = name 180 | 181 | if !manual { 182 | UpdateChecked = true 183 | 184 | // Semver Check? 185 | if ver.Version != cur.Version && cur.Version != "vLocal" { 186 | UpdateAvailable = &ver 187 | } 188 | 189 | return ver, nil 190 | } 191 | 192 | // This goes here and signals else where that we got the request back 193 | UpdateChecked = true 194 | 195 | // Semver Check? 196 | if ver.Version != cur.Version && (manual || cur.Version != "vLocal") { 197 | UpdateAvailable = &ver 198 | aI, ok := gh["assets"] 199 | if ok { 200 | a, aok := aI.([]interface{}) 201 | if aok { 202 | UpdateData = a 203 | } 204 | } 205 | } 206 | 207 | return ver, nil 208 | } 209 | 210 | func WaitPrintUpdateAvailable() { 211 | if os.Getenv("{{ .CLI.CLI_NAME }}_UPDATES_DISABLED") != "" { 212 | return 213 | } 214 | for i := 0; i < 20 && !UpdateStarted && !UpdateChecked && !UpdateErrored; i++ { 215 | time.Sleep(50 * time.Millisecond) 216 | } 217 | PrintUpdateAvailable() 218 | } 219 | 220 | func PrintUpdateAvailable() { 221 | if UpdateChecked && UpdateAvailable != nil { 222 | fmt.Printf(updateMessage, verinfo.Version, UpdateAvailable.Version) 223 | } 224 | } 225 | 226 | func InstallUpdate() (err error) { 227 | fmt.Printf("Installing {{ .CLI.cliName }}@%s\n", UpdateAvailable.Version) 228 | 229 | if UpdateData == nil { 230 | return fmt.Errorf("No update available") 231 | } 232 | /* 233 | vers, err := json.MarshalIndent(UpdateData, "", " ") 234 | if err == nil { 235 | fmt.Println(string(vers)) 236 | } 237 | */ 238 | 239 | fmt.Println("OS/Arch", verinfo.BuildOS, verinfo.BuildArch) 240 | 241 | url := "" 242 | for _, Asset := range UpdateData { 243 | asset := Asset.(map[string]interface{}) 244 | U := asset["browser_download_url"].(string) 245 | u := strings.ToLower(U) 246 | 247 | osOk, archOk := false, false 248 | 249 | switch verinfo.BuildOS { 250 | case "linux": 251 | if strings.Contains(u, "linux") { 252 | osOk = true 253 | } 254 | 255 | case "darwin": 256 | if strings.Contains(u, "darwin") { 257 | osOk = true 258 | } 259 | 260 | case "windows": 261 | if strings.Contains(u, "windows") { 262 | osOk = true 263 | } 264 | } 265 | 266 | switch verinfo.BuildArch { 267 | case "amd64": 268 | if strings.Contains(u, "x86_64"){ 269 | archOk = true 270 | } 271 | case "arm64": 272 | if strings.Contains(u, "arm64"){ 273 | archOk = true 274 | } 275 | case "arm": 276 | if strings.Contains(u, "arm") && !strings.Contains(u, "arm64"){ 277 | archOk = true 278 | } 279 | } 280 | 281 | if osOk && archOk { 282 | url = u 283 | break 284 | } 285 | } 286 | 287 | fmt.Println("Download URL: ", url) 288 | 289 | switch verinfo.BuildOS { 290 | case "linux": 291 | fallthrough 292 | case "darwin": 293 | 294 | return downloadAndInstall(url) 295 | 296 | case "windows": 297 | fmt.Println("Please downlaod and install manually from the link above.") 298 | return nil 299 | } 300 | 301 | fmt.Println() 302 | return nil 303 | } 304 | 305 | func downloadAndInstall(url string) error { 306 | req := gorequest.New() 307 | if os.Getenv("GITHUB_TOKEN") != "" { 308 | req = req.SetBasicAuth("github-token", os.Getenv("GITHUB_TOKEN")) 309 | } 310 | 311 | // handle redirects 312 | req.RedirectPolicy(func(r gorequest.Request, via []gorequest.Request) error { 313 | viaURL := []string{} 314 | for _, v := range via { 315 | viaURL = append(viaURL, v.URL.String()) 316 | } 317 | return nil 318 | }) 319 | 320 | resp, content, errs := req.Get(url).EndBytes() 321 | 322 | check := "http2: server sent GOAWAY and closed the connection" 323 | if len(errs) != 0 && !strings.Contains(errs[0].Error(), check) { 324 | fmt.Println("errs:", errs) 325 | fmt.Println("resp:", resp) 326 | return errs[0] 327 | } 328 | 329 | if len(errs) != 0 || resp.StatusCode >= 400 { 330 | return fmt.Errorf("Error %v - %s", resp.StatusCode, string(content)) 331 | } 332 | 333 | tmpfile, err := ioutil.TempFile("", "example") 334 | if err != nil { 335 | return err 336 | } 337 | 338 | // defer os.Remove(tmpfile.Name()) // clean up 339 | 340 | if _, err := tmpfile.Write(content); err != nil { 341 | return err 342 | } 343 | if err := tmpfile.Close(); err != nil { 344 | return err 345 | } 346 | 347 | ex, err := os.Executable() 348 | if err != nil { 349 | return err 350 | } 351 | 352 | real, err := filepath.EvalSymlinks(ex) 353 | if err != nil { 354 | return err 355 | } 356 | 357 | // Sudo copy the file 358 | cmdStr := fmt.Sprintf("export OWNER=$(ls -l %s | awk '{ print $3 \":\" $4 }') && sudo mv %s %s-v%s && sudo cp %s %s && sudo chown $OWNER %s && sudo chmod 0755 %s", 359 | real, // get owner 360 | real, real, verinfo.Version, // backup 361 | tmpfile.Name(), real, // cp 362 | real, // chown 363 | real, // chmod 364 | ) 365 | // fmt.Println("CMDSTR:", cmdStr) 366 | cmd := exec.Command("/bin/sh", "-c", cmdStr) 367 | 368 | // prep stdin for password 369 | stdin, err := cmd.StdinPipe() 370 | if err != nil { 371 | return err 372 | } 373 | 374 | go func() { 375 | defer stdin.Close() 376 | io.WriteString(stdin, "values written to stdin are passed to cmd's standard input") 377 | }() 378 | 379 | stdoutStderr, err := cmd.CombinedOutput() 380 | fmt.Printf("%s\n", stdoutStderr) 381 | if err != nil { 382 | return err 383 | } 384 | 385 | UpdateAvailable = nil 386 | UpdateData = nil 387 | return nil 388 | } 389 | --------------------------------------------------------------------------------