Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3a79286
feat: add commands
ogzhanolguncu Jul 14, 2025
30531e7
feat: allow configuring name,desc and version
ogzhanolguncu Jul 14, 2025
53e33f9
feat: pass env to cli
ogzhanolguncu Jul 14, 2025
cc55b1d
feat: match the initial impl
ogzhanolguncu Jul 14, 2025
82d3393
feat: add new progress aniamtion
ogzhanolguncu Jul 14, 2025
d05bce4
feat: add tracker step for each phase
ogzhanolguncu Jul 14, 2025
9e1b02a
refactor: improve animations and errors
ogzhanolguncu Jul 15, 2025
aa7cde2
feat: use proper orchestrafor managing steps and trackers
ogzhanolguncu Jul 15, 2025
454828e
refactor: rename build to run
ogzhanolguncu Jul 15, 2025
f058067
refactor: remove UI logic from api
ogzhanolguncu Jul 15, 2025
7e1a3b4
chore: remove redundant commands
ogzhanolguncu Jul 15, 2025
238c658
refactor: remove ui bloat
ogzhanolguncu Jul 15, 2025
e8ab3dc
feat: add colors for make it distinguishable
ogzhanolguncu Jul 15, 2025
2cd7fa4
Merge branch 'main' into ENG-1903
ogzhanolguncu Jul 15, 2025
9152767
fix: steps
ogzhanolguncu Jul 15, 2025
52fe00d
fix: code rabbit issues
ogzhanolguncu Jul 15, 2025
f80dd82
feat: add proper flag parsing logic
ogzhanolguncu Jul 16, 2025
496a994
refactor: show help if required args are missing
ogzhanolguncu Jul 16, 2025
b8f964f
feat: add missing commands
ogzhanolguncu Jul 16, 2025
1f5b476
fix: code rabbit comments
ogzhanolguncu Jul 16, 2025
74f407a
refactor: fix redundancy
ogzhanolguncu Jul 16, 2025
2a8f0ed
Merge branch 'main' into ENG-1903
ogzhanolguncu Jul 16, 2025
20068a1
refactor: improve sub spinner
ogzhanolguncu Jul 16, 2025
02d2597
refactor: move duplicated spinner loop
ogzhanolguncu Jul 16, 2025
b1fd291
refactor: remove some commands for later
ogzhanolguncu Jul 17, 2025
8c960a1
Merge branch 'main' into ENG-1903
ogzhanolguncu Jul 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions go/cmd/cli/cli/command.go
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)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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
}
268 changes: 268 additions & 0 deletions go/cmd/cli/cli/flag.go
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
}
}
Comment thread
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
}
Loading
Loading