-
Notifications
You must be signed in to change notification settings - Fork 627
feat: Unkey Deploy CLI #3564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
feat: Unkey Deploy CLI #3564
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
3a79286
feat: add commands
ogzhanolguncu 30531e7
feat: allow configuring name,desc and version
ogzhanolguncu 53e33f9
feat: pass env to cli
ogzhanolguncu cc55b1d
feat: match the initial impl
ogzhanolguncu 82d3393
feat: add new progress aniamtion
ogzhanolguncu d05bce4
feat: add tracker step for each phase
ogzhanolguncu 9e1b02a
refactor: improve animations and errors
ogzhanolguncu aa7cde2
feat: use proper orchestrafor managing steps and trackers
ogzhanolguncu 454828e
refactor: rename build to run
ogzhanolguncu f058067
refactor: remove UI logic from api
ogzhanolguncu 7e1a3b4
chore: remove redundant commands
ogzhanolguncu 238c658
refactor: remove ui bloat
ogzhanolguncu e8ab3dc
feat: add colors for make it distinguishable
ogzhanolguncu 2cd7fa4
Merge branch 'main' into ENG-1903
ogzhanolguncu 9152767
fix: steps
ogzhanolguncu 52fe00d
fix: code rabbit issues
ogzhanolguncu f80dd82
feat: add proper flag parsing logic
ogzhanolguncu 496a994
refactor: show help if required args are missing
ogzhanolguncu b8f964f
feat: add missing commands
ogzhanolguncu 1f5b476
fix: code rabbit comments
ogzhanolguncu 74f407a
refactor: fix redundancy
ogzhanolguncu 2a8f0ed
Merge branch 'main' into ENG-1903
ogzhanolguncu 20068a1
refactor: improve sub spinner
ogzhanolguncu 02d2597
refactor: move duplicated spinner loop
ogzhanolguncu b1fd291
refactor: remove some commands for later
ogzhanolguncu 8c960a1
Merge branch 'main' into ENG-1903
ogzhanolguncu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| ) | ||
|
|
||
| // Action represents a command handler function that receives context and the parsed command | ||
| type Action func(context.Context, *Command) error | ||
|
|
||
| // Command represents a CLI command with its configuration and runtime state | ||
| type Command struct { | ||
| // Configuration | ||
| Name string // Command name (e.g., "deploy", "version") | ||
| Usage string // Short description shown in help | ||
| Description string // Longer description for detailed help | ||
| Version string // Version string (only used for root command) | ||
| Commands []*Command // Subcommands | ||
| Flags []Flag // Available flags for this command | ||
| Action Action // Function to execute when command is run | ||
| Aliases []string // Alternative names for this command | ||
|
|
||
| // Runtime state (populated during parsing) | ||
| args []string // Non-flag arguments passed to command | ||
| flagMap map[string]Flag // Map for O(1) flag lookup | ||
| parent *Command // Parent command (for building usage paths) | ||
| } | ||
|
|
||
| // Args returns the non-flag arguments passed to the command | ||
| // Example: "mycli deploy myapp" -> Args() returns ["myapp"] | ||
| func (c *Command) Args() []string { | ||
| return c.args | ||
| } | ||
|
|
||
| // String returns the value of a string flag by name | ||
| // Returns empty string if flag doesn't exist or isn't a StringFlag | ||
| func (c *Command) String(name string) string { | ||
| if flag, ok := c.flagMap[name]; ok { | ||
| if sf, ok := flag.(*StringFlag); ok { | ||
| return sf.Value() | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // Bool returns the value of a boolean flag by name | ||
| // Returns false if flag doesn't exist or isn't a BoolFlag | ||
| func (c *Command) Bool(name string) bool { | ||
| if flag, ok := c.flagMap[name]; ok { | ||
| if bf, ok := flag.(*BoolFlag); ok { | ||
| return bf.Value() | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // Int returns the value of an integer flag by name | ||
| // Returns 0 if flag doesn't exist or isn't an IntFlag | ||
| func (c *Command) Int(name string) int { | ||
| if flag, ok := c.flagMap[name]; ok { | ||
| if inf, ok := flag.(*IntFlag); ok { | ||
| return inf.Value() | ||
| } | ||
| } | ||
| return 0 | ||
| } | ||
|
|
||
| // Float returns the value of a float flag by name | ||
| // Returns 0.0 if flag doesn't exist or isn't a FloatFlag | ||
| func (c *Command) Float(name string) float64 { | ||
| if flag, ok := c.flagMap[name]; ok { | ||
| if ff, ok := flag.(*FloatFlag); ok { | ||
| return ff.Value() | ||
| } | ||
| } | ||
| return 0.0 | ||
| } | ||
|
|
||
| // StringSlice returns the value of a string slice flag by name | ||
| // Returns empty slice if flag doesn't exist or isn't a StringSliceFlag | ||
| func (c *Command) StringSlice(name string) []string { | ||
| if flag, ok := c.flagMap[name]; ok { | ||
| if ssf, ok := flag.(*StringSliceFlag); ok { | ||
| return ssf.Value() | ||
| } | ||
| } | ||
| return []string{} | ||
| } | ||
|
|
||
| // Run executes the command with the given arguments (typically os.Args) | ||
| // This is the main entry point for CLI execution | ||
| func (c *Command) Run(ctx context.Context, args []string) error { | ||
| if len(args) == 0 { | ||
| return fmt.Errorf("no arguments provided") | ||
| } | ||
| // Parse arguments starting from index 1 (skip program name) | ||
| return c.parse(ctx, args[1:]) | ||
| } | ||
|
|
||
| // Exit provides a clean way to exit with an error message and code | ||
| // This is a convenience function that prints the message and calls os.Exit | ||
| func Exit(message string, code int) error { | ||
| fmt.Println(message) | ||
| os.Exit(code) | ||
| return nil // unreachable but satisfies error interface | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,268 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "strconv" | ||
| "strings" | ||
| ) | ||
|
|
||
| // Flag represents a command line flag interface | ||
| // All flag types must implement these methods | ||
| type Flag interface { | ||
| Name() string // The flag name (without dashes) | ||
| Usage() string // Help text describing the flag | ||
| Required() bool // Whether this flag is mandatory | ||
| Parse(value string) error // Parse string value into the flag's type | ||
| IsSet() bool // Whether the flag was explicitly set by user | ||
| } | ||
|
|
||
| // baseFlag contains common fields and methods shared by all flag types | ||
| type baseFlag struct { | ||
| name string // Flag name | ||
| usage string // Help description | ||
| envVar string // Environment variable to check for default | ||
| required bool // Whether flag is mandatory | ||
| set bool // Whether user explicitly provided this flag | ||
| } | ||
|
|
||
| // Name returns the flag name | ||
| func (b *baseFlag) Name() string { return b.name } | ||
|
|
||
| // Usage returns the flag's help text | ||
| func (b *baseFlag) Usage() string { return b.usage } | ||
|
|
||
| // Required returns whether this flag is mandatory | ||
| func (b *baseFlag) Required() bool { return b.required } | ||
|
|
||
| // IsSet returns whether the user explicitly provided this flag | ||
| func (b *baseFlag) IsSet() bool { return b.set } | ||
|
|
||
| // EnvVar returns the environment variable name for this flag | ||
| func (b *baseFlag) EnvVar() string { return b.envVar } | ||
|
|
||
| // StringFlag represents a string command line flag | ||
| type StringFlag struct { | ||
| baseFlag | ||
| value string // Current value | ||
| } | ||
|
|
||
| // Parse sets the flag value from a string | ||
| func (f *StringFlag) Parse(value string) error { | ||
| f.value = value | ||
| f.set = true | ||
| return nil | ||
| } | ||
|
|
||
| // Value returns the current string value | ||
| func (f *StringFlag) Value() string { return f.value } | ||
|
|
||
| // BoolFlag represents a boolean command line flag | ||
| type BoolFlag struct { | ||
| baseFlag | ||
| value bool // Current value | ||
| } | ||
|
|
||
| // Parse sets the flag value from a string | ||
| // Empty string means the flag was provided without a value (--flag), which sets it to true | ||
| // Otherwise parses as boolean: "true", "false", "1", "0", etc. | ||
| func (f *BoolFlag) Parse(value string) error { | ||
| if value == "" { | ||
| f.value = true | ||
| f.set = true | ||
| return nil | ||
| } | ||
| parsed, err := strconv.ParseBool(value) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid boolean value: %s", value) | ||
| } | ||
| f.value = parsed | ||
| f.set = true | ||
| return nil | ||
| } | ||
|
|
||
| // Value returns the current boolean value | ||
| func (f *BoolFlag) Value() bool { return f.value } | ||
|
|
||
| // IntFlag represents an integer command line flag | ||
| type IntFlag struct { | ||
| baseFlag | ||
| value int // Current value | ||
| } | ||
|
|
||
| // Parse sets the flag value from a string | ||
| func (f *IntFlag) Parse(value string) error { | ||
| parsed, err := strconv.Atoi(value) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid integer value: %s", value) | ||
| } | ||
| f.value = parsed | ||
| f.set = true | ||
| return nil | ||
| } | ||
|
|
||
| // Value returns the current integer value | ||
| func (f *IntFlag) Value() int { return f.value } | ||
|
|
||
| // FloatFlag represents a float64 command line flag | ||
| type FloatFlag struct { | ||
| baseFlag | ||
| value float64 // Current value | ||
| } | ||
|
|
||
| // Parse sets the flag value from a string | ||
| func (f *FloatFlag) Parse(value string) error { | ||
| parsed, err := strconv.ParseFloat(value, 64) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid float value: %s", value) | ||
| } | ||
| f.value = parsed | ||
| f.set = true | ||
| return nil | ||
| } | ||
|
|
||
| // Value returns the current float64 value | ||
| func (f *FloatFlag) Value() float64 { return f.value } | ||
|
|
||
| // StringSliceFlag represents a string slice command line flag | ||
| type StringSliceFlag struct { | ||
| baseFlag | ||
| value []string // Current value | ||
| } | ||
|
|
||
| // parseCommaSeparated splits a comma-separated string into a slice of trimmed non-empty strings | ||
| func (f *StringSliceFlag) parseCommaSeparated(value string) []string { | ||
| if value == "" { | ||
| return []string{} | ||
| } | ||
| parts := strings.Split(value, ",") | ||
| result := make([]string, 0, len(parts)) | ||
| for _, part := range parts { | ||
| trimmed := strings.TrimSpace(part) | ||
| if trimmed != "" { | ||
| result = append(result, trimmed) | ||
| } | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| // Parse sets the flag value from a string (comma-separated values) | ||
| func (f *StringSliceFlag) Parse(value string) error { | ||
| f.value = f.parseCommaSeparated(value) | ||
| f.set = true | ||
| return nil | ||
| } | ||
|
|
||
| // Value returns the current string slice value | ||
| func (f *StringSliceFlag) Value() []string { return f.value } | ||
|
|
||
| // String creates a new string flag with environment variable support | ||
| // If envVar is provided and set, it will be used as the default value | ||
| func String(name, usage, defaultValue, envVar string, required bool) *StringFlag { | ||
| flag := &StringFlag{ | ||
| baseFlag: baseFlag{ | ||
| name: name, | ||
| usage: usage, | ||
| envVar: envVar, | ||
| required: required, | ||
| }, | ||
| value: defaultValue, | ||
| } | ||
| // Check environment variable for default value | ||
| if envVar != "" { | ||
| if envValue := os.Getenv(envVar); envValue != "" { | ||
| flag.value = envValue | ||
| flag.set = true // Mark as set since env var was found | ||
| } | ||
| } | ||
|
ogzhanolguncu marked this conversation as resolved.
|
||
| return flag | ||
| } | ||
|
|
||
| // Bool creates a new boolean flag with environment variable support | ||
| func Bool(name, usage, envVar string, required bool) *BoolFlag { | ||
| flag := &BoolFlag{ | ||
| baseFlag: baseFlag{ | ||
| name: name, | ||
| usage: usage, | ||
| envVar: envVar, | ||
| required: required, | ||
| }, | ||
| } | ||
| // Check environment variable for default value | ||
| if envVar != "" { | ||
| if envValue := os.Getenv(envVar); envValue != "" { | ||
| if parsed, err := strconv.ParseBool(envValue); err == nil { | ||
| flag.value = parsed | ||
| flag.set = true // Mark as set since env var was found | ||
| } | ||
| } | ||
| } | ||
| return flag | ||
| } | ||
|
|
||
| // Int creates a new integer flag with environment variable support | ||
| func Int(name, usage string, defaultValue int, envVar string, required bool) *IntFlag { | ||
| flag := &IntFlag{ | ||
| baseFlag: baseFlag{ | ||
| name: name, | ||
| usage: usage, | ||
| envVar: envVar, | ||
| required: required, | ||
| }, | ||
| value: defaultValue, | ||
| } | ||
| // Check environment variable for default value | ||
| if envVar != "" { | ||
| if envValue := os.Getenv(envVar); envValue != "" { | ||
| if parsed, err := strconv.Atoi(envValue); err == nil { | ||
| flag.value = parsed | ||
| flag.set = true // Mark as set since env var was found | ||
| } | ||
| } | ||
| } | ||
| return flag | ||
| } | ||
|
|
||
| // Float creates a new float flag with environment variable support | ||
| func Float(name, usage string, defaultValue float64, envVar string, required bool) *FloatFlag { | ||
| flag := &FloatFlag{ | ||
| baseFlag: baseFlag{ | ||
| name: name, | ||
| usage: usage, | ||
| envVar: envVar, | ||
| required: required, | ||
| }, | ||
| value: defaultValue, | ||
| } | ||
| // Check environment variable for default value | ||
| if envVar != "" { | ||
| if envValue := os.Getenv(envVar); envValue != "" { | ||
| if parsed, err := strconv.ParseFloat(envValue, 64); err == nil { | ||
| flag.value = parsed | ||
| flag.set = true // Mark as set since env var was found | ||
| } | ||
| } | ||
| } | ||
| return flag | ||
| } | ||
|
|
||
| // StringSlice creates a new string slice flag with environment variable support | ||
| func StringSlice(name, usage string, defaultValue []string, envVar string, required bool) *StringSliceFlag { | ||
| flag := &StringSliceFlag{ | ||
| baseFlag: baseFlag{ | ||
| name: name, | ||
| usage: usage, | ||
| envVar: envVar, | ||
| required: required, | ||
| }, | ||
| value: defaultValue, | ||
| } | ||
| // Check environment variable for default value | ||
| if envVar != "" { | ||
| if envValue := os.Getenv(envVar); envValue != "" { | ||
| flag.value = flag.parseCommaSeparated(envValue) | ||
| flag.set = true // Mark as set since env var was found | ||
| } | ||
| } | ||
| return flag | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.