.gitignore 🔗
@@ -43,7 +43,7 @@ Thumbs.db
**/.crush/**
-crush
+/crush
manpages/
completions/
Kujtim Hoxha created
.gitignore | 2
cmd/logs.go | 86 +++++++++++++++++++++++++++++++++++++++++++++
cmd/root.go | 3 +
go.mod | 7 +++
go.sum | 14 +++++++
pkg/config/resolve.go | 1
pkg/log/log.go | 32 ++++++++++++++++
7 files changed, 143 insertions(+), 2 deletions(-)
@@ -43,7 +43,7 @@ Thumbs.db
**/.crush/**
-crush
+/crush
manpages/
completions/
@@ -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
+ },
+}
@@ -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
@@ -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 (
@@ -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=
@@ -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(),
@@ -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))
+}