diff --git a/cmd/root.go b/cmd/root.go index ab7339a..b183308 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,12 +4,37 @@ import ( "fmt" "github.com/spf13/cobra" "github.com/vybdev/vyb/cmd/template" + "github.com/vybdev/vyb/config" + "github.com/vybdev/vyb/logging" "os" ) +var logLevel string +var debugLogging bool + var rootCmd = &cobra.Command{ Use: "vyb", Short: "vyb is a CLI tool that uses AI to help you iteratively develop applications faster", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + cfg, err := config.Load(".") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if logLevel == "" { + logLevel = cfg.Logging.Level + } + + if logLevel == "" { + logLevel = "info" + } + + if err := logging.Init(logLevel); err != nil { + fmt.Println(err) + os.Exit(1) + } + }, Run: func(cmd *cobra.Command, args []string) { // If no subcommand is provided, print usage. fmt.Println(cmd.UsageString()) @@ -25,6 +50,8 @@ func Execute() { } func init() { + rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "", "log level (e.g. debug, info, warn, error, fatal, panic)") + rootCmd.PersistentFlags().BoolVar(&debugLogging, "debug", false, "enable request/response debug logging") err := template.Register(rootCmd) if err != nil { fmt.Println(err) diff --git a/cmd/template/template.go b/cmd/template/template.go index 5e18764..4b29c45 100644 --- a/cmd/template/template.go +++ b/cmd/template/template.go @@ -3,6 +3,7 @@ package template import ( "fmt" "github.com/vybdev/vyb/config" + "github.com/vybdev/vyb/logging" "os" "path/filepath" "strings" @@ -168,9 +169,9 @@ func execute(cmd *cobra.Command, args []string, def *Definition) error { } if len(patchResult.ChangedModules) > 0 { - fmt.Println("Warning: metadata is stale. Run 'vyb update' to refresh.") + logging.Log.Warn("metadata is stale. Run 'vyb update' to refresh.") for moduleName, change := range patchResult.ChangedModules { - fmt.Printf(" - Module %s changed by %.2f%%\n", moduleName, change.ChangePercentage()) + logging.Log.Warnf(" - Module %s changed by %.2f%%\n", moduleName, change.ChangePercentage()) } } @@ -196,12 +197,12 @@ func execute(cmd *cobra.Command, args []string, def *Definition) error { } } - fmt.Printf("The following files will be included in the request:\n") + logging.Log.Infof("The following files will be included in the request:\n") for _, file := range files { if relTarget != nil && file == *relTarget { - fmt.Printf(" %s <-- TARGET\n", file) + logging.Log.Infof(" %s <-- TARGET\n", file) } else { - fmt.Printf(" %s\n", file) + logging.Log.Infof(" %s\n", file) } } @@ -264,11 +265,11 @@ func execute(cmd *cobra.Command, args []string, def *Definition) error { return err } - fmt.Printf("Change summary: %s\n\n", proposal.Summary) - fmt.Printf("Change description: %s\n\n", proposal.Description) - fmt.Printf("Changed files: \n") + logging.Log.Infof("Change summary: %s\n\n", proposal.Summary) + logging.Log.Infof("Change description: %s\n\n", proposal.Description) + logging.Log.Infof("Changed files: \n") for _, file := range proposal.Proposals { - fmt.Printf(" %s -- delete? %v\n", file.FileName, file.Delete) + logging.Log.Infof(" %s -- delete? %v\n", file.FileName, file.Delete) } return nil @@ -282,7 +283,7 @@ func applyProposals(absRoot string, proposals []payload.FileChangeProposal) erro if err := os.Remove(absPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to delete file %s: %w", absPath, err) } - fmt.Printf("Deleted file: %s\n", prop.FileName) + logging.Log.Infof("Deleted file: %s\n", prop.FileName) } else { dir := filepath.Dir(absPath) if err := os.MkdirAll(dir, 0755); err != nil { @@ -291,7 +292,7 @@ func applyProposals(absRoot string, proposals []payload.FileChangeProposal) erro if err := os.WriteFile(absPath, []byte(prop.Content), 0644); err != nil { return fmt.Errorf("failed to write to file %s: %w", absPath, err) } - fmt.Printf("Modified file: %s\n", prop.FileName) + logging.Log.Infof("Modified file: %s\n", prop.FileName) } } return nil diff --git a/cmd/update.go b/cmd/update.go index ce3b945..b7e8ca6 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -1,8 +1,8 @@ package cmd import ( - "fmt" "github.com/spf13/cobra" + "github.com/vybdev/vyb/logging" "github.com/vybdev/vyb/workspace/project" "os" ) @@ -20,8 +20,8 @@ func Update(_ *cobra.Command, _ []string) { // for now, `vyb update` only works when executed on the root of the project err := project.Update(".") if err != nil { - fmt.Printf("Error creating metadata: %v\n", err) + logging.Log.Fatalf("Error creating metadata: %v\n", err) os.Exit(1) } - fmt.Println("Project metadata updated successfully.") + logging.Log.Info("Project metadata updated successfully.") } diff --git a/config/config.go b/config/config.go index a89d9a9..3ff1b8b 100644 --- a/config/config.go +++ b/config/config.go @@ -29,6 +29,13 @@ import ( //nolint:revive // field name is intentionally simple type Config struct { Provider string `yaml:"provider"` + Logging `yaml:"logging"` +} + +// Logging captures logging-specific settings. +type Logging struct { + Level string `yaml:"level"` + RequestResponseDebug bool `yaml:"request-response-debug"` } // defaultProvider is used when no configuration file exists or it cannot @@ -39,7 +46,13 @@ const defaultProvider = "openai" // Default returns a Config populated with hard-coded defaults. It should // be used whenever .vyb/config.yaml is missing. func Default() *Config { - return &Config{Provider: defaultProvider} + return &Config{ + Provider: defaultProvider, + Logging: Logging{ + Level: "info", + RequestResponseDebug: false, + }, + } } // Load reads .vyb/config.yaml located under projectRoot. When the file @@ -77,4 +90,4 @@ func LoadFS(fsys fs.FS) (*Config, error) { cfg.Provider = defaultProvider } return &cfg, nil -} +} \ No newline at end of file diff --git a/logging/logging.go b/logging/logging.go new file mode 100644 index 0000000..2138b19 --- /dev/null +++ b/logging/logging.go @@ -0,0 +1,27 @@ +package logging + +import ( + "github.com/sirupsen/logrus" + "os" +) + +var ( + // Log is the default logger for the application. + Log = logrus.New() +) + +// Init initializes the logger with the given log level. +func Init(level string) error { + logLevel, err := logrus.ParseLevel(level) + if err != nil { + return err + } + + Log.SetLevel(logLevel) + Log.SetOutput(os.Stderr) + Log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + }) + + return nil +} diff --git a/workspace/project/annotation.go b/workspace/project/annotation.go index 0dae880..e76a736 100644 --- a/workspace/project/annotation.go +++ b/workspace/project/annotation.go @@ -5,6 +5,7 @@ import ( "github.com/vybdev/vyb/config" "github.com/vybdev/vyb/llm" "github.com/vybdev/vyb/llm/payload" + "github.com/vybdev/vyb/logging" "io/fs" "strings" ) @@ -47,10 +48,10 @@ func annotate(cfg *config.Config, metadata *Metadata, sysfs fs.FS) error { // Launch annotation tasks. for _, m := range modules { if m.Annotation != nil { - fmt.Printf("module %q already has an annotation, skipping...\n", m.Name) + logging.Log.Infof("module %q already has an annotation, skipping...\n", m.Name) continue } - fmt.Printf("module %q doesn't have annotation\n", m.Name) + logging.Log.Infof("module %q doesn't have annotation\n", m.Name) // Capture m for the goroutine. go func(mod *Module) { // Wait for all submodules to complete. @@ -136,7 +137,7 @@ func addOrUpdateSelfContainedContext(cfg *config.Config, m *Module, sysfs fs.FS) SubModulesPublicContexts: subContexts, } - fmt.Printf("annotating module %q\n", m.Name) + logging.Log.Infof("annotating module %q\n", m.Name) // System prompt instructing the LLM to summarize code into JSON schema. systemMessage := `You are a prompt engineer, structuring information about an application's code base @@ -165,7 +166,7 @@ Each type of context should be as descriptive as possible, using around one thou context, err := llm.GetModuleContext(cfg, systemMessage, req) - fmt.Printf(" Got response for module %q\n", m.Name) + logging.Log.Infof(" Got response for module %q\n", m.Name) if err != nil { return fmt.Errorf("failed to call llm provider: %w", err) @@ -177,17 +178,17 @@ Each type of context should be as descriptive as possible, using around one thou if context.InternalContext != "" { if m.Annotation.InternalContext != "" { - fmt.Printf(" Overriding field `InternalContext` of module %q.\n", m.Name) + logging.Log.Infof(" Overriding field `InternalContext` of module %q.\n", m.Name) } else { - fmt.Printf(" Creating field `InternalContext` of module %q.\n", m.Name) + logging.Log.Infof(" Creating field `InternalContext` of module %q.\n", m.Name) } m.Annotation.InternalContext = context.InternalContext } if context.PublicContext != "" { if m.Annotation.PublicContext != "" { - fmt.Printf(" Overriding field `PublicContext` of module %q.\n", m.Name) + logging.Log.Infof(" Overriding field `PublicContext` of module %q.\n", m.Name) } else { - fmt.Printf(" Creating field `PublicContext` of module %q.\n", m.Name) + logging.Log.Infof(" Creating field `PublicContext` of module %q.\n", m.Name) } m.Annotation.PublicContext = context.PublicContext } @@ -297,7 +298,7 @@ Return your answer as JSON following the schema you have been provided.` } mod.Annotation.ExternalContext = ext.ExternalContext } else { - fmt.Printf(" WARNING: module %q not found in module map\n", ext.Name) + logging.Log.Warnf(" WARNING: module %q not found in module map\n", ext.Name) } } @@ -320,4 +321,4 @@ func collectAllModules(root *Module) []*Module { } walk(root) return out -} +} \ No newline at end of file