Skip to content

Commit

Permalink
feat: cli
Browse files Browse the repository at this point in the history
  • Loading branch information
skewb1k committed Mar 5, 2025
1 parent 89687da commit ce8fe18
Show file tree
Hide file tree
Showing 12 changed files with 394 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ linters:
- exhaustive # checks exhaustiveness of enum switch statements
- exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions
- fatcontext # detects nested contexts in loops
- forbidigo # forbids identifiers
# - forbidigo # forbids identifiers
# - funlen # tool for detection of long functions
- gocheckcompilerdirectives # validates go compiler directive comments (//go:)
# - gochecknoglobals # checks that no global variables exist
Expand Down
6 changes: 1 addition & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,4 @@ test:

.PHONY: build
build:
go build -o .out/bin cmd/cli/main.go

.PHONY: run
run:
go run cmd/cli/main.go
go build -o .out/flow cmd/cli/main.go
1 change: 0 additions & 1 deletion cmd/cli/commands.go

This file was deleted.

70 changes: 21 additions & 49 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
@@ -1,67 +1,39 @@
package main

import (
"context"
"fmt"
"log"
"log/slog"
"os"

"github.com/spf13/cobra"
"github.com/flowtemplates/cli/internal/config"
"github.com/flowtemplates/cli/internal/controller/cli"
"github.com/flowtemplates/cli/internal/repository/templates"
"github.com/flowtemplates/cli/internal/service"
)

var rootCmd = &cobra.Command{
Use: "flow",
Short: "TemplatesFlow CLI",
Long: `A sample CLI application that showcases:
- Command/subcommand structure
- Flag parsing
- Auto-generated help and usage information
- Shell completion support for commands and flags`,
Run: func(cmd *cobra.Command, args []string) {
log.Println("Welcome to your CLI app!")
},
}

// completionCmd generates shell completion scripts.
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion scripts",
Long: `To load completions:
const defaultConfigName = "flow"

Bash:
$ source <(app completion bash)
func run() error {
ctx := context.Background()

Zsh:
$ source <(app completion zsh)
cfg, err := config.GetConfig(defaultConfigName)
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}

Fish:
$ app completion fish | source
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

PowerShell:
PS> app completion powershell | Out-String | Invoke-Expression
`,
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Run: func(cmd *cobra.Command, args []string) {
var err error
switch args[0] {
case "bash":
err = rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
err = rootCmd.GenZshCompletion(os.Stdout)
case "fish":
err = rootCmd.GenFishCompletion(os.Stdout, true)
case "powershell":
err = rootCmd.GenPowerShellCompletionWithDesc(os.Stdout)
}
r := templates.New(cfg.TemplatesFolder)
s := service.New(r)
c := cli.New(s, logger)

log.Fatalf("%s\n", err)
},
return c.Cmd().ExecuteContext(ctx) // nolint: wrapcheck
}

func main() {
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.AddCommand(completionCmd)

if err := rootCmd.Execute(); err != nil {
log.Fatalf("%s\n", err)
if err := run(); err != nil {
log.Fatalln(err)
}
}
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/flowtemplates/cli

go 1.24.0

require github.com/spf13/cobra v1.9.1
require (
github.com/spf13/cobra v1.9.1
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
87 changes: 87 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package config

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"gopkg.in/yaml.v3"
)

// Config struct defining expected fields
type Config struct {
TemplatesFolder string `json:"templatesFolder" yaml:"templatesFolder"`
}

// fileExists checks if a file exists
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}

// findConfig searches for the config file in the current directory and its parents
func findConfig(baseName string) (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get pwd: %w", err)
}

extensions := []string{".json", ".yaml", ".yml"}

for {
for _, ext := range extensions {
configPath := filepath.Join(dir, baseName+ext)
if fileExists(configPath) {
return configPath, nil
}
}

parentDir := filepath.Dir(dir)
if parentDir == dir {
break
}
dir = parentDir
}

return "", fmt.Errorf("config file %s[.json/.yaml/.yml] not found", baseName)
}

// ReadConfigFile reads and parses a JSON or YAML file into the provided struct
func ReadConfigFile(filename string, v *Config) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}

// Detect file type and parse accordingly
switch filepath.Ext(filename) {
case ".json":
err = json.Unmarshal(data, v)
case ".yaml", ".yml":
err = yaml.Unmarshal(data, v)
default:
return fmt.Errorf("unsupported file format: %s", filename)
}

if err != nil {
return fmt.Errorf("failed to parse %s: %w", filename, err)
}

return nil
}

// GetConfig searches for a config file, reads, and parses it into a Config struct
func GetConfig(baseName string) (*Config, error) {
configPath, err := findConfig(baseName)
if err != nil {
return nil, err
}

var config Config
if err := ReadConfigFile(configPath, &config); err != nil {
return nil, err
}

return &config, nil
}
77 changes: 77 additions & 0 deletions internal/controller/cli/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cli

import (
"encoding/json"
"fmt"

"github.com/spf13/cobra"
)

func (c CliController) newListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all components",
RunE: func(cmd *cobra.Command, args []string) error {
jsonOutput, _ := cmd.Flags().GetBool("json")

templates, err := c.service.ListTemplates()
if err != nil {
return fmt.Errorf("failed to list templates: %w", err)
}

if jsonOutput {
data, _ := json.Marshal(templates)
fmt.Printf("%s\n", data)
} else {
for _, c := range templates {
fmt.Printf("- %s\n", c)
}
}
return nil
},
}

cmd.Flags().Bool("json", false, "Output in JSON format")
return cmd
}

func (c CliController) newAddCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Add component to dir",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
templateName := args[0]
err := c.service.Add(templateName, args[1:]...)
if err != nil {
return fmt.Errorf("failed to add: %w", err)
}

return nil
},
}

cmd.Flags().Bool("json", false, "Output in JSON format")
return cmd
}

func (c CliController) newGetCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: "Get component",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println(args)
templateName := args[0]
err := c.service.Add(templateName, args[1:]...)
if err != nil {
return fmt.Errorf("failed to get template: %w", err)
}

return nil
},
}

cmd.Flags().Bool("json", false, "Output in JSON format")
return cmd
}
37 changes: 37 additions & 0 deletions internal/controller/cli/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cli

import (
"log/slog"

"github.com/spf13/cobra"
)

type iService interface {
ListTemplates() ([]string, error)
Add(templateName string, dests ...string) error
}

type CliController struct {
service iService
logger *slog.Logger
}

func New(service iService, logger *slog.Logger) *CliController {
return &CliController{
service: service,
logger: logger,
}
}

func (c CliController) Cmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "flow",
Short: "FlowTemplates CLI",
}

rootCmd.AddCommand(c.newListCmd())
rootCmd.AddCommand(c.newGetCmd())
rootCmd.AddCommand(c.newAddCmd())

return rootCmd
}
11 changes: 11 additions & 0 deletions internal/repository/source/repo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package source

type SourceRepo struct {
baseDir string
}

func New(baseDir string) *SourceRepo {
return &SourceRepo{
baseDir: baseDir,
}
}
Loading

0 comments on commit ce8fe18

Please sign in to comment.