diff --git a/.gitignore b/.gitignore index ff6f6265b947a990e3bfa256d4cf6a9dc0447150..b28e5a0c727163e8f3585522e680d1df2ad6e621 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,7 @@ Thumbs.db **/.crush/** -crush +/crush manpages/ completions/ diff --git a/cmd/logs.go b/cmd/logs.go new file mode 100644 index 0000000000000000000000000000000000000000..aebc290cb1153bb68c4c7cdfb0c78e0d51985554 --- /dev/null +++ b/cmd/logs.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "path/filepath" + "time" + + "github.com/charmbracelet/crush/pkg/config" + "github.com/charmbracelet/crush/pkg/env" + "github.com/charmbracelet/log/v2" + "github.com/nxadm/tail" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(logsCmd) +} + +var logsCmd = &cobra.Command{ + Use: "logs", + Short: "View crush logs", + Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring purposes.`, + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := cmd.Flags().GetString("cwd") + if err != nil { + return fmt.Errorf("failed to get current working directory: %v", err) + } + cfg, err := config.Load(cwd, env.New()) + if err != nil { + return fmt.Errorf("failed to load configuration: %v", err) + } + t, err := tail.TailFile(filepath.Join(cfg.Options.DataDirectory, "logs", "crush.log"), tail.Config{Follow: true, ReOpen: true, Logger: tail.DiscardingLogger}) + if err != nil { + return fmt.Errorf("failed to tail log file: %v", err) + } + + // Print the text of each received line + for line := range t.Lines { + var data map[string]any + if err := json.Unmarshal([]byte(line.Text), &data); err != nil { + continue + } + msg := data["msg"] + level := data["level"] + otherData := []any{} + for k, v := range data { + switch k { + case "msg", "level", "time": + continue + case "source": + source, ok := v.(map[string]any) + if !ok { + continue + } + sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64))) + otherData = append(otherData, "source", sourceFile) + + default: + otherData = append(otherData, k, v) + } + } + log.SetTimeFunction(func(_ time.Time) time.Time { + // parse the timestamp from the log line if available + t, err := time.Parse(time.RFC3339, data["time"].(string)) + if err != nil { + return time.Now() // fallback to current time if parsing fails + } + return t + }) + switch level { + case "INFO": + log.Info(msg, otherData...) + case "DEBUG": + log.Debug(msg, otherData...) + case "ERROR": + log.Error(msg, otherData...) + case "WARN": + log.Warn(msg, otherData...) + default: + log.Info(msg, otherData...) + } + } + return nil + }, +} diff --git a/cmd/root.go b/cmd/root.go index 4ba90e0452a104c909ab0539ace93dbf07093e17..5cd3eb5355aa3f4580b97203e65eb2953cf42d65 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -292,9 +292,10 @@ func Execute() { } func init() { + rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory") + rootCmd.Flags().BoolP("help", "h", false, "Help") rootCmd.Flags().BoolP("debug", "d", false, "Debug") - rootCmd.Flags().StringP("cwd", "c", "", "Current working directory") rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode") // Add format flag with validation logic diff --git a/go.mod b/go.mod index b0f04c00392c6ad3cb661542f5b9ce1b961da684..25bc8d66440e76b3842d7e3a11770b57cddb4fa9 100644 --- a/go.mod +++ b/go.mod @@ -41,9 +41,16 @@ require ( ) require ( + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/log v0.4.2 // indirect + github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/nxadm/tail v1.4.11 // indirect github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c // indirect github.com/spf13/cast v1.7.1 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) require ( diff --git a/go.sum b/go.sum index ff706c7c003e9047d2b3e5bf8239646571c67321..266487d545c1dd62f6d83a56c6efc6d816a7ac19 100644 --- a/go.sum +++ b/go.sum @@ -82,10 +82,16 @@ github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc= github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY= github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c h1:177KMz8zHRlEZJsWzafbKYh6OdjgvTspoH+UjaxgIXY= github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 h1:X0tsNa2UHCKNw+illiavosasVzqioRo32SRV35iwr2I= github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= +github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= +github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= +github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE= +github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM= github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413 h1:L07QkDqRF274IZ2UJ/mCTL8DR95efU9BNWLYCDXEjvQ= github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME= @@ -120,6 +126,7 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= @@ -201,6 +208,8 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP8= github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -322,6 +331,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -368,6 +378,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/pkg/config/resolve.go b/pkg/config/resolve.go index 483d383b22fa15ee6d5fc6713388919c82f350cc..9e88a8f06e3572bd557ac09abdef8e84ada71f9e 100644 --- a/pkg/config/resolve.go +++ b/pkg/config/resolve.go @@ -25,6 +25,7 @@ type shellVariableResolver struct { func NewShellVariableResolver(env env.Env) VariableResolver { return &shellVariableResolver{ + env: env, shell: shell.NewShell( &shell.Options{ Env: env.Env(), diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000000000000000000000000000000000000..1cfcf6522c1e8c9114d2e9f86a68ba786f1df2f0 --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,32 @@ +package log + +import ( + "log/slog" + "path/filepath" + + "github.com/charmbracelet/crush/pkg/config" + + "gopkg.in/natefinch/lumberjack.v2" +) + +func Init(cfg *config.Config) { + logRotator := &lumberjack.Logger{ + Filename: filepath.Join(cfg.Options.DataDirectory, "logs", "crush.log"), + MaxSize: 10, // Max size in MB + MaxBackups: 0, // Number of backups + MaxAge: 30, // Days + Compress: false, // Enable compression + } + + level := slog.LevelInfo + if cfg.Options.Debug { + level = slog.LevelDebug + } + + logger := slog.NewJSONHandler(logRotator, &slog.HandlerOptions{ + Level: level, + AddSource: true, + }) + + slog.SetDefault(slog.New(logger)) +}