Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 63 additions & 8 deletions internal/app/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"strings"

"fuku/internal/app/generator"
"fuku/internal/app/runner"
"fuku/internal/app/ui/wire"
"fuku/internal/config"
Expand All @@ -14,12 +15,19 @@ import (

const (
Usage = `Usage:
fuku init [--profile=<NAME>] [--service=<NAME>] [--force] [--dry-run]
Generate fuku.yaml from template
fuku --run=<PROFILE> Run services with specified profile (with TUI)
fuku --run=<PROFILE> --no-ui Run services without TUI
fuku help Show help
fuku version Show version

Examples:
fuku init Generate fuku.yaml with defaults
fuku init --profile=dev --service=backend
Generate with custom profile and service
fuku init --force Overwrite existing fuku.yaml
fuku init --dry-run Preview generated file without writing
fuku --run=default Run all services with TUI
fuku --run=core --no-ui Run core services without TUI
fuku --run=minimal Run minimal services with TUI
Expand Down Expand Up @@ -48,43 +56,58 @@ type CLI interface {

// cli represents the command-line interface for the application
type cli struct {
cfg *config.Config
runner runner.Runner
ui wire.UI
log logger.Logger
cfg *config.Config
runner runner.Runner
ui wire.UI
generator generator.Generator
log logger.Logger
}

// NewCLI creates a new cli instance
func NewCLI(
cfg *config.Config,
runner runner.Runner,
ui wire.UI,
generator generator.Generator,
log logger.Logger,
) CLI {
return &cli{
cfg: cfg,
runner: runner,
ui: ui,
log: log,
cfg: cfg,
runner: runner,
ui: ui,
generator: generator,
log: log,
}
}

// Run processes command-line arguments and executes commands
func (c *cli) Run(args []string) (int, error) {
noUI := false
force := false
dryRun := false
profile := config.DefaultProfile
initProfile := ""
initService := ""

var remainingArgs []string

for _, arg := range args {
switch {
case arg == "--no-ui":
noUI = true
case arg == "--force":
force = true
case arg == "--dry-run":
dryRun = true
case strings.HasPrefix(arg, "--run="):
profile = strings.TrimPrefix(arg, "--run=")
if profile == "" {
profile = config.DefaultProfile
}
case strings.HasPrefix(arg, "--profile="):
initProfile = strings.TrimPrefix(arg, "--profile=")
case strings.HasPrefix(arg, "--service="):
initService = strings.TrimPrefix(arg, "--service=")
default:
remainingArgs = append(remainingArgs, arg)
}
Expand All @@ -97,6 +120,8 @@ func (c *cli) Run(args []string) (int, error) {
cmd := remainingArgs[0]

switch cmd {
case "init", "--init", "-i":
return c.handleInit(initProfile, initService, force, dryRun)
case "help", "--help", "-h":
return c.handleHelp()
case "version", "--version", "-v":
Expand Down Expand Up @@ -176,6 +201,36 @@ func (c *cli) handleVersion() (int, error) {
return 0, nil
}

// handleInit generates a fuku.yaml file from template
func (c *cli) handleInit(profileName, serviceName string, force bool, dryRun bool) (int, error) {
opts := generator.DefaultOptions()
if profileName != "" {
opts.ProfileName = profileName
}

if serviceName != "" {
opts.ServiceName = serviceName
}

c.log.Debug().Msgf("Generating fuku.yaml (profile=%s, service=%s, force=%v, dryRun=%v)", opts.ProfileName, opts.ServiceName, force, dryRun)

if err := c.generator.Generate(opts, force, dryRun); err != nil {
c.log.Error().Err(err).Msg("Failed to generate fuku.yaml")
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

return 1, err
}

if !dryRun {
fmt.Println("Generated fuku.yaml")
fmt.Println("\nNext steps:")
fmt.Println(" 1. Edit fuku.yaml to configure your services")
fmt.Printf(" 2. Run 'fuku --run=%s' to start services\n", opts.ProfileName)
}

return 0, nil
}

// handleUnknown handles unknown commands
func (c *cli) handleUnknown() (int, error) {
c.log.Debug().Msg("Unknown command")
Expand Down
15 changes: 10 additions & 5 deletions internal/app/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"

"fuku/internal/app/generator"
"fuku/internal/app/runner"
"fuku/internal/app/ui/wire"
"fuku/internal/config"
Expand All @@ -28,9 +29,10 @@ func Test_NewCLI(t *testing.T) {
mockUI := func(ctx context.Context, profile string) (*tea.Program, error) {
return nil, nil
}
mockGenerator := generator.NewMockGenerator(ctrl)
mockLogger := logger.NewMockLogger(ctrl)

cliInstance := NewCLI(cfg, mockRunner, mockUI, mockLogger)
cliInstance := NewCLI(cfg, mockRunner, mockUI, mockGenerator, mockLogger)
assert.NotNil(t, cliInstance)

instance, ok := cliInstance.(*cli)
Expand All @@ -39,6 +41,7 @@ func Test_NewCLI(t *testing.T) {
assert.Equal(t, cfg, instance.cfg)
assert.Equal(t, mockRunner, instance.runner)
assert.NotNil(t, instance.ui)
assert.Equal(t, mockGenerator, instance.generator)
assert.Equal(t, mockLogger, instance.log)
}

Expand All @@ -47,17 +50,19 @@ func Test_Run(t *testing.T) {
defer ctrl.Finish()

mockRunner := runner.NewMockRunner(ctrl)
mockGenerator := generator.NewMockGenerator(ctrl)
cfg := config.DefaultConfig()
mockUI := wire.UI(func(ctx context.Context, profile string) (*tea.Program, error) {
return nil, nil
})
mockLogger := logger.NewMockLogger(ctrl)

c := &cli{
cfg: cfg,
runner: mockRunner,
ui: mockUI,
log: mockLogger,
cfg: cfg,
runner: mockRunner,
ui: mockUI,
generator: mockGenerator,
log: mockLogger,
}

tests := []struct {
Expand Down
86 changes: 86 additions & 0 deletions internal/app/generator/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package generator

import (
"bytes"
"embed"
"fmt"
"os"
"text/template"

"fuku/internal/config/logger"
)

const (
templatePath = "templates/fuku.yaml.tmpl"
fileName = "fuku.yaml"
)

//go:embed templates/fuku.yaml.tmpl
var templateFS embed.FS

// Options contains the configuration for generating fuku.yaml
type Options struct {
ProfileName string
ServiceName string
}

// DefaultOptions returns sensible defaults for generation
func DefaultOptions() Options {
return Options{
ProfileName: "default",
ServiceName: "api",
}
}

// Generator defines the interface for generating fuku.yaml
type Generator interface {
Generate(opts Options, force bool, dryRun bool) error
}

type generator struct {
log logger.Logger
}

// NewGenerator creates a new generator instance
func NewGenerator(log logger.Logger) Generator {
return &generator{
log: log,
}
}

// Generate creates a fuku.yaml file from the template
func (g *generator) Generate(opts Options, force bool, dryRun bool) error {
if !dryRun && !force {
if _, err := os.Stat(fileName); err == nil {
return fmt.Errorf("file %s already exists, use --force to overwrite", fileName)
}
}

tmplContent, err := templateFS.ReadFile(templatePath)
if err != nil {
return fmt.Errorf("failed to read template: %w", err)
}

tmpl, err := template.New("fuku.yaml").Parse(string(tmplContent))
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}

var buf bytes.Buffer
if err := tmpl.Execute(&buf, opts); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}

if dryRun {
fmt.Print(buf.String())
return nil
}

if err := os.WriteFile(fileName, buf.Bytes(), 0600); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}

g.log.Info().Msgf("Generated %s", fileName)

return nil
}
54 changes: 54 additions & 0 deletions internal/app/generator/generator_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading