From 6144805b4b63527574cfd3a834e034face533b27 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Thu, 25 Dec 2025 21:19:11 +0800 Subject: [PATCH 1/2] refactor(logging): replace logrus with stdlib slog and add path helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace github.com/sirupsen/logrus with log/slog across all packages - Add model/path.go with centralized path helper functions - Fix typo: SocketTopicProccessor -> SocketTopicProcessor - Remove logrus dependency from go.mod šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/cli/main.go | 4 +-- cmd/daemon/main.go | 2 +- commands/alias.go | 23 ++++++------ commands/auth.go | 4 +-- commands/dotfiles_pull.go | 30 ++++++++-------- commands/dotfiles_push.go | 18 +++++----- commands/gc.go | 37 ++++++++------------ commands/logger.go | 25 +++++++------ commands/ls.go | 4 +-- commands/query.go | 6 ++-- commands/query_test.go | 2 -- commands/sync.go | 4 +-- commands/track.go | 34 +++++++++--------- commands/track_test.go | 8 ++--- commands/utils.go | 5 ++- daemon/handlers.go | 2 +- daemon/handlers_test.go | 10 +++--- go.mod | 3 +- go.sum | 3 -- model/alias.go | 13 ++++--- model/api.base.go | 20 +++++------ model/api.go | 9 +++-- model/command.go | 31 ++++++++-------- model/db.go | 31 ++++++++-------- model/dotfile.go | 11 +++--- model/dotfile_apps.go | 39 ++++++++++----------- model/dotfile_ghostty.go | 15 ++++---- model/exclude.go | 7 ++-- model/handshake.go | 16 ++++----- model/path.go | 74 +++++++++++++++++++++++++++++++++++++++ 30 files changed, 270 insertions(+), 220 deletions(-) create mode 100644 model/path.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 3a2a511..dbd2c19 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -3,12 +3,12 @@ package main import ( "context" "fmt" + "log/slog" "os" "time" "github.com/malamtime/cli/commands" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/uptrace/uptrace-go/uptrace" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/attribute" @@ -110,7 +110,7 @@ func main() { } err = app.Run(os.Args) if err != nil { - logrus.Errorln(err) + slog.Error("CLI error", slog.Any("err", err)) } commands.CloseLogger() } diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 0eacc59..468c293 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -88,7 +88,7 @@ func main() { defer syncCircuitBreakerService.Stop() } - go daemon.SocketTopicProccessor(msg) + go daemon.SocketTopicProcessor(msg) // Start CCUsage service if enabled (v1 - ccusage CLI based) if cfg.CCUsage != nil && cfg.CCUsage.Enabled != nil && *cfg.CCUsage.Enabled { diff --git a/commands/alias.go b/commands/alias.go index e447952..148785c 100644 --- a/commands/alias.go +++ b/commands/alias.go @@ -2,11 +2,11 @@ package commands import ( "context" + "log/slog" "os" "strings" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -51,25 +51,24 @@ func importAliases(c *cli.Context) error { ctx, span := commandTracer.Start(c.Context, "alias-import", trace.WithSpanKind(trace.SpanKindClient)) defer span.End() SetupLogger(os.ExpandEnv("$HOME/" + model.COMMAND_BASE_STORAGE_FOLDER)) - logrus.SetLevel(logrus.TraceLevel) isFullyRefresh := c.Bool("fully-refresh") span.SetAttributes(attribute.Bool("fully-refresh", isFullyRefresh)) zshConfigFile, err := expandPath(c.String("zsh-config")) if err != nil { - logrus.Errorln(err) + slog.Error("failed to expand zsh config path", slog.Any("err", err)) return err } fishConfigFile, err := expandPath(c.String("fish-config")) if err != nil { - logrus.Errorln(err) + slog.Error("failed to expand fish config path", slog.Any("err", err)) return err } config, err := configService.ReadConfigFile(ctx) if err != nil { - logrus.Errorln(err) + slog.Error("failed to read config file", slog.Any("err", err)) return err } @@ -81,10 +80,10 @@ func importAliases(c *cli.Context) error { if _, err := os.Stat(zshConfigFile); err == nil { aliases, err := parseZshAliases(ctx, zshConfigFile) if err != nil { - logrus.Errorln("Failed to parse zsh aliases:", err) + slog.Error("Failed to parse zsh aliases", slog.Any("err", err)) return err } - logrus.Traceln("Found aliases in zsh configuration", len(aliases)) + slog.Debug("Found aliases in zsh configuration", slog.Int("count", len(aliases))) err = model.SendAliasesToServer( ctx, mainEndpoint, @@ -94,7 +93,7 @@ func importAliases(c *cli.Context) error { zshConfigFile, ) if err != nil { - logrus.Errorln("Failed to send aliases to server:", err) + slog.Error("Failed to send aliases to server", slog.Any("err", err)) return err } } @@ -102,10 +101,10 @@ func importAliases(c *cli.Context) error { if _, err := os.Stat(fishConfigFile); err == nil { aliases, err := parseFishAliases(ctx, fishConfigFile) if err != nil { - logrus.Errorln("Failed to parse fish aliases:", err) + slog.Error("Failed to parse fish aliases", slog.Any("err", err)) return err } - logrus.Traceln("Found aliases in fish configuration", len(aliases)) + slog.Debug("Found aliases in fish configuration", slog.Int("count", len(aliases))) err = model.SendAliasesToServer( ctx, mainEndpoint, @@ -115,12 +114,12 @@ func importAliases(c *cli.Context) error { fishConfigFile, ) if err != nil { - logrus.Errorln("Failed to send aliases to server:", err) + slog.Error("Failed to send aliases to server", slog.Any("err", err)) return err } } - logrus.Infoln("Successfully imported aliases") + slog.Info("Successfully imported aliases") return nil } diff --git a/commands/auth.go b/commands/auth.go index 65493e1..7c6eb15 100644 --- a/commands/auth.go +++ b/commands/auth.go @@ -3,6 +3,7 @@ package commands import ( "context" "fmt" + "log/slog" "os" "time" @@ -11,7 +12,6 @@ import ( "github.com/malamtime/cli/model" "github.com/pelletier/go-toml/v2" "github.com/pkg/browser" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/trace" ) @@ -102,7 +102,7 @@ func ApplyTokenByHandshake(_ctx context.Context, config model.ShellTimeConfig) ( feLink := fmt.Sprintf("%s/cli/integration?hid=%s", config.WebEndpoint, hid) if err := browser.OpenURL(feLink); err != nil { - logrus.Errorln(err) + slog.Error("failed to open browser", slog.Any("err", err)) } color.Green.Println(fmt.Sprintf("Open %s to continue", feLink)) diff --git a/commands/dotfiles_pull.go b/commands/dotfiles_pull.go index 2192b79..0100563 100644 --- a/commands/dotfiles_pull.go +++ b/commands/dotfiles_pull.go @@ -2,11 +2,11 @@ package commands import ( "fmt" + "log/slog" "os" "github.com/malamtime/cli/model" "github.com/pterm/pterm" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -37,7 +37,7 @@ func printPullResults(result map[model.DotfileAppName][]dotfilePullFileResult, d // No files to process if totalProcessed == 0 && totalFailed == 0 && totalSkipped == 0 { - logrus.Infoln("No dotfiles found to process") + slog.Info("No dotfiles found to process") pterm.Info.Println("No dotfiles to process") return } @@ -117,7 +117,7 @@ func printPullResults(result map[model.DotfileAppName][]dotfilePullFileResult, d } // Log for debugging - logrus.Infof("Pull complete - Processed: %d, Skipped: %d, Failed: %d", totalProcessed, totalSkipped, totalFailed) + slog.Info("Pull complete", slog.Int("processed", totalProcessed), slog.Int("skipped", totalSkipped), slog.Int("failed", totalFailed)) } func pullDotfiles(c *cli.Context) error { @@ -131,7 +131,7 @@ func pullDotfiles(c *cli.Context) error { config, err := configService.ReadConfigFile(ctx) if err != nil { - logrus.Errorln(err) + slog.Error("failed to read config file", slog.Any("err", err)) return err } @@ -169,15 +169,15 @@ func pullDotfiles(c *cli.Context) error { } // Fetch dotfiles from server - logrus.Infof("Fetching dotfiles from server...") + slog.Info("Fetching dotfiles from server...") resp, err := model.FetchDotfilesFromServer(ctx, mainEndpoint, filter) if err != nil { - logrus.Errorln("Failed to fetch dotfiles from server:", err) + slog.Error("Failed to fetch dotfiles from server", slog.Any("err", err)) return err } if resp == nil || len(resp.Data.FetchUser.Dotfiles.Apps) == 0 { - logrus.Infoln("No dotfiles found on server") + slog.Info("No dotfiles found on server") fmt.Println("\nšŸ“­ No dotfiles found on server") return nil } @@ -188,18 +188,18 @@ func pullDotfiles(c *cli.Context) error { appName := model.DotfileAppName(appData.App) app, exists := appHandlers[appName] if !exists { - logrus.Warnf("Unknown app type: %s", appData.App) + slog.Warn("Unknown app type", slog.String("app", appData.App)) continue } - logrus.Infof("Processing %s dotfiles...", appData.App) + slog.Info("Processing dotfiles...", slog.String("app", appData.App)) // Collect files to process for this app filesToProcess := make(map[string]string) for _, file := range appData.Files { if len(file.Records) == 0 { - logrus.Debugf("No records found for %s", file.Path) + slog.Debug("No records found", slog.String("path", file.Path)) continue } @@ -246,7 +246,7 @@ func pullDotfiles(c *cli.Context) error { // Check which files are different equalityMap, err := app.IsEqual(ctx, filesToProcess) if err != nil { - logrus.Warnf("Failed to check file equality for %s: %v", appData.App, err) + slog.Warn("Failed to check file equality", slog.String("app", appData.App), slog.Any("err", err)) } // Filter out files that are already equal @@ -255,7 +255,7 @@ func pullDotfiles(c *cli.Context) error { for path, content := range filesToProcess { if isEqual, exists := equalityMap[path]; exists && isEqual { - logrus.Debugf("Skipping %s - content is identical", path) + slog.Debug("Skipping - content is identical", slog.String("path", path)) result[appName] = append(result[appName], dotfilePullFileResult{ path: path, isSkipped: true, @@ -267,7 +267,7 @@ func pullDotfiles(c *cli.Context) error { } if len(filesToUpdate) == 0 { - logrus.Infof("All %s files are up to date", appData.App) + slog.Info("All files are up to date", slog.String("app", appData.App)) continue } @@ -275,12 +275,12 @@ func pullDotfiles(c *cli.Context) error { // Backup files that will be modified (handles dry-run internally) if err := app.Backup(ctx, pathsToActuallyBackup, dryRun); err != nil { - logrus.Warnf("Failed to backup files for %s: %v", appData.App, err) + slog.Warn("Failed to backup files", slog.String("app", appData.App), slog.Any("err", err)) } // Save the updated files (handles dry-run internally) if err := app.Save(ctx, filesToUpdate, dryRun); err != nil { - logrus.Errorf("Failed to save files for %s: %v", appData.App, err) + slog.Error("Failed to save files", slog.String("app", appData.App), slog.Any("err", err)) for f := range filesToUpdate { results = append(results, dotfilePullFileResult{ path: f, diff --git a/commands/dotfiles_push.go b/commands/dotfiles_push.go index a4ee1b3..ad443e6 100644 --- a/commands/dotfiles_push.go +++ b/commands/dotfiles_push.go @@ -2,10 +2,10 @@ package commands import ( "fmt" + "log/slog" "os" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -21,7 +21,7 @@ func pushDotfiles(c *cli.Context) error { config, err := configService.ReadConfigFile(ctx) if err != nil { - logrus.Errorln(err) + slog.Error("failed to read config file", slog.Any("err", err)) return err } @@ -66,7 +66,7 @@ func pushDotfiles(c *cli.Context) error { if app, ok := appMap[appName]; ok { selectedApps = append(selectedApps, app) } else { - logrus.Warnf("Unknown app: %s", appName) + slog.Warn("Unknown app", slog.String("app", appName)) } } } @@ -74,31 +74,31 @@ func pushDotfiles(c *cli.Context) error { // Collect all dotfiles var allDotfiles []model.DotfileItem for _, app := range selectedApps { - logrus.Infof("Collecting dotfiles for %s", app.Name()) + slog.Info("Collecting dotfiles", slog.String("app", app.Name())) dotfiles, err := app.CollectDotfiles(ctx) if err != nil { - logrus.Errorf("Failed to collect dotfiles for %s: %v", app.Name(), err) + slog.Error("Failed to collect dotfiles", slog.String("app", app.Name()), slog.Any("err", err)) continue } allDotfiles = append(allDotfiles, dotfiles...) } if len(allDotfiles) == 0 { - logrus.Infoln("No dotfiles found to push") + slog.Info("No dotfiles found to push") return nil } // Send to server - logrus.Infof("Pushing %d dotfiles to server", len(allDotfiles)) + slog.Info("Pushing dotfiles to server", slog.Int("count", len(allDotfiles))) userID, err := model.SendDotfilesToServer(ctx, mainEndpoint, allDotfiles) if err != nil { - logrus.Errorln("Failed to send dotfiles to server:", err) + slog.Error("Failed to send dotfiles to server", slog.Any("err", err)) return err } // Generate web link for managing dotfiles webLink := fmt.Sprintf("%s/users/%d/settings/dotfiles", config.WebEndpoint, userID) - logrus.Infof("Successfully pushed dotfiles. Manage them at: %s", webLink) + slog.Info("Successfully pushed dotfiles", slog.String("webLink", webLink)) fmt.Printf("\nāœ… Successfully pushed %d dotfiles to server\n", len(allDotfiles)) fmt.Printf("šŸ“ Manage your dotfiles at: %s\n", webLink) diff --git a/commands/gc.go b/commands/gc.go index bcf5ee8..61e419c 100644 --- a/commands/gc.go +++ b/commands/gc.go @@ -3,11 +3,11 @@ package commands import ( "bytes" "fmt" + "log/slog" "os" "sort" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/trace" ) @@ -72,15 +72,14 @@ func commandGC(c *cli.Context) error { cmd := new(model.Command) _, err := cmd.FromLineBytes(raw) if err != nil { - err = fmt.Errorf("failed to parse command from line: %v", err) - logrus.Warnln(err) + slog.Warn("failed to parse command from line", slog.Any("err", err)) continue } postCommands[i] = cmd } if postCount == 0 { - logrus.Traceln("no post commands need to be clean") + slog.Debug("no post commands need to be clean") return nil } @@ -144,24 +143,21 @@ func commandGC(c *cli.Context) error { if _, err := os.Stat(originalPreFile); err == nil { if err := os.Rename(originalPreFile, preBackupFile); err != nil { - err = fmt.Errorf("failed to backup PRE_FILE: %v", err) - logrus.Warnln(err) - return err + slog.Warn("failed to backup PRE_FILE", slog.Any("err", err)) + return fmt.Errorf("failed to backup PRE_FILE: %v", err) } } if _, err := os.Stat(originalPostFile); err == nil { if err := os.Rename(originalPostFile, postBackupFile); err != nil { - err = fmt.Errorf("failed to backup POST_FILE: %v", err) - logrus.Warnln(err) - return err + slog.Warn("failed to backup POST_FILE", slog.Any("err", err)) + return fmt.Errorf("failed to backup POST_FILE: %v", err) } } if _, err := os.Stat(originalCursorFile); err == nil { if err := os.Rename(originalCursorFile, cursorBackupFile); err != nil { - err = fmt.Errorf("failed to backup CURSOR_FILE: %v", err) - logrus.Warnln(err) - return err + slog.Warn("failed to backup CURSOR_FILE", slog.Any("err", err)) + return fmt.Errorf("failed to backup CURSOR_FILE: %v", err) } } @@ -174,9 +170,8 @@ func commandGC(c *cli.Context) error { preFileContent.Write(line) } if err := os.WriteFile(originalPreFile, preFileContent.Bytes(), 0644); err != nil { - err = fmt.Errorf("failed to write new PRE_FILE: %v", err) - logrus.Warnln(err) - return err + slog.Warn("failed to write new PRE_FILE", slog.Any("err", err)) + return fmt.Errorf("failed to write new PRE_FILE: %v", err) } postFileContent := bytes.Buffer{} @@ -189,17 +184,15 @@ func commandGC(c *cli.Context) error { } if err := os.WriteFile(originalPostFile, postFileContent.Bytes(), 0644); err != nil { - err = fmt.Errorf("failed to write new POST_FILE: %v", err) - logrus.Warnln(err) - return err + slog.Warn("failed to write new POST_FILE", slog.Any("err", err)) + return fmt.Errorf("failed to write new POST_FILE: %v", err) } lastCursorNano := lastCursor.UnixNano() lastCursorBytes := []byte(fmt.Sprintf("%d", lastCursorNano)) if err := os.WriteFile(originalCursorFile, lastCursorBytes, 0644); err != nil { - err = fmt.Errorf("failed to write new CURSOR_FILE: %v", err) - logrus.Warnln(err) - return err + slog.Warn("failed to write new CURSOR_FILE", slog.Any("err", err)) + return fmt.Errorf("failed to write new CURSOR_FILE: %v", err) } // TODO: delete $HOME/.config/malamtime/ folder diff --git a/commands/logger.go b/commands/logger.go index ee5501d..10ffb86 100644 --- a/commands/logger.go +++ b/commands/logger.go @@ -3,8 +3,6 @@ package commands import ( "log/slog" "os" - - "github.com/sirupsen/logrus" ) var loggerFile *os.File @@ -13,7 +11,6 @@ var SKIP_LOGGER_SETTINGS = false func SetupLogger(baseFolder string) { if SKIP_LOGGER_SETTINGS { - logrus.SetReportCaller(true) return } // TODO: change to size based logger selector @@ -22,13 +19,13 @@ func SetupLogger(baseFolder string) { if _, err := os.Stat(logFilePath); os.IsNotExist(err) { err := os.MkdirAll(baseFolder, 0755) if err != nil { - slog.Error("[ShellTime.xyz] failed to create log directory: ", slog.Any("err", err)) + slog.Error("[ShellTime.xyz] failed to create log directory", slog.Any("err", err)) return } _, err = os.Create(logFilePath) if err != nil { - slog.Error("[ShellTime.xyz] failed to create log file: ", slog.Any("err", err)) + slog.Error("[ShellTime.xyz] failed to create log file", slog.Any("err", err)) return } } @@ -36,13 +33,19 @@ func SetupLogger(baseFolder string) { f, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755) if err != nil { os.Stdout.WriteString(err.Error()) - slog.Error("[ShellTime.xyz] on setup logger error: ", slog.Any("err", err)) + slog.Error("[ShellTime.xyz] on setup logger error", slog.Any("err", err)) + return } loggerFile = f - logrus.SetReportCaller(true) - logrus.SetLevel(logrus.TraceLevel) - logrus.SetOutput(loggerFile) - logrus.Traceln("Setting up logger with version: ", commitID) + + // Create a new slog handler that writes to the file + handler := slog.NewTextHandler(f, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }) + slog.SetDefault(slog.New(handler)) + + slog.Debug("Setting up logger", slog.String("version", commitID)) } func CloseLogger() { @@ -52,7 +55,7 @@ func CloseLogger() { if loggerFile == nil { return } - logrus.Traceln("going to close...") + slog.Debug("going to close...") loggerFile.Close() loggerFile = nil } diff --git a/commands/ls.go b/commands/ls.go index 2d74818..b0d5779 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -4,6 +4,7 @@ package commands import ( "encoding/json" "fmt" + "log/slog" "os" "strconv" "time" @@ -11,7 +12,6 @@ import ( "github.com/gookit/color" "github.com/malamtime/cli/model" "github.com/olekukonko/tablewriter" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/trace" ) @@ -81,7 +81,7 @@ func commandList(c *cli.Context) error { postCommand := new(model.Command) _, err := postCommand.FromLineBytes(line) if err != nil { - logrus.Errorln("Failed to parse post command: ", err, string(line)) + slog.Error("Failed to parse post command", slog.Any("err", err), slog.String("line", string(line))) continue } diff --git a/commands/query.go b/commands/query.go index bc7e1bc..4a85701 100644 --- a/commands/query.go +++ b/commands/query.go @@ -3,6 +3,7 @@ package commands import ( "context" "fmt" + "log/slog" "os" "os/exec" "runtime" @@ -12,7 +13,6 @@ import ( "github.com/briandowns/spinner" "github.com/gookit/color" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -51,7 +51,7 @@ func commandQuery(c *cli.Context) error { // Get system context systemContext, err := getSystemContext(query) if err != nil { - logrus.Warnf("Failed to get system context: %v", err) + slog.Warn("Failed to get system context", slog.Any("err", err)) } s := spinner.New(spinner.CharSets[35], 200*time.Millisecond) @@ -77,7 +77,7 @@ func commandQuery(c *cli.Context) error { // Check auto-run configuration cfg, err := configService.ReadConfigFile(ctx) if err != nil { - logrus.Warnf("Failed to read config for auto-run check: %v", err) + slog.Warn("Failed to read config for auto-run check", slog.Any("err", err)) // If can't read config, just display the command displayCommand(newCommand) return nil diff --git a/commands/query_test.go b/commands/query_test.go index a02dd64..871984b 100644 --- a/commands/query_test.go +++ b/commands/query_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" @@ -29,7 +28,6 @@ type queryTestSuite struct { // SetupSuite runs once before all tests func (s *queryTestSuite) SetupSuite() { - logrus.SetLevel(logrus.TraceLevel) otel.SetTracerProvider(noop.NewTracerProvider()) SKIP_LOGGER_SETTINGS = true } diff --git a/commands/sync.go b/commands/sync.go index cb70f90..56e79fa 100644 --- a/commands/sync.go +++ b/commands/sync.go @@ -1,10 +1,10 @@ package commands import ( + "log/slog" "os" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/trace" ) @@ -34,7 +34,7 @@ func commandSync(c *cli.Context) error { config, err := configService.ReadConfigFile(ctx) if err != nil { - logrus.Errorln(err) + slog.Error("failed to read config file", slog.Any("err", err)) return err } diff --git a/commands/track.go b/commands/track.go index aa23599..fe46a3d 100644 --- a/commands/track.go +++ b/commands/track.go @@ -3,12 +3,12 @@ package commands import ( "context" "fmt" + "log/slog" "os" "time" "github.com/malamtime/cli/daemon" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -58,16 +58,16 @@ func commandTrack(c *cli.Context) error { defer span.End() SetupLogger(os.ExpandEnv("$HOME/" + model.COMMAND_BASE_STORAGE_FOLDER)) - logrus.Traceln(c.Args().First()) + slog.Debug("track command args", slog.String("first", c.Args().First())) config, err := configService.ReadConfigFile(ctx) if err != nil { - logrus.Errorln(err) + slog.Error("failed to read config file", slog.Any("err", err)) return err } hostname, err := os.Hostname() if err != nil { - logrus.Errorln(err) + slog.Error("failed to get hostname", slog.Any("err", err)) return err } @@ -91,7 +91,7 @@ func commandTrack(c *cli.Context) error { // Check if command should be excluded if model.ShouldExcludeCommand(cmdCommand, config.Exclude) { - logrus.Debugf("Command excluded by pattern: %s", cmdCommand) + slog.Debug("Command excluded by pattern", slog.String("command", cmdCommand)) return nil } @@ -104,7 +104,7 @@ func commandTrack(c *cli.Context) error { err = instance.DoUpdate(result) } if err != nil { - logrus.Errorln(err) + slog.Error("failed to save/update command", slog.Any("err", err)) return err } @@ -135,7 +135,7 @@ func trySyncLocalToServer( } if len(postFileContent) == 0 || lineCount == 0 { - logrus.Traceln("Not enough records to sync, current count:", lineCount) + slog.Debug("Not enough records to sync", slog.Int("lineCount", lineCount)) return nil } @@ -151,7 +151,7 @@ func trySyncLocalToServer( sysInfo, err := model.GetOSAndVersion() if err != nil { - logrus.Warnln(err) + slog.Warn("failed to get OS version", slog.Any("err", err)) sysInfo = &model.SysInfo{ Os: "unknown", Version: "unknown", @@ -173,7 +173,7 @@ func trySyncLocalToServer( postCommand := new(model.Command) recordingTime, err := postCommand.FromLineBytes(line) if err != nil { - logrus.Errorln("Failed to parse post command: ", err, string(line)) + slog.Error("Failed to parse post command", slog.Any("err", err), slog.String("line", string(line))) continue } @@ -202,7 +202,7 @@ func trySyncLocalToServer( // Check if command should be excluded during sync if model.ShouldExcludeCommand(postCommand.Command, config.Exclude) { - logrus.Tracef("Command excluded during sync: %s", postCommand.Command) + slog.Debug("Command excluded during sync", slog.String("command", postCommand.Command)) continue } @@ -231,7 +231,7 @@ func trySyncLocalToServer( } if len(trackingData) == 0 { - logrus.Traceln("no tracking data need to be sync") + slog.Debug("no tracking data need to be sync") return nil } @@ -239,14 +239,14 @@ func trySyncLocalToServer( if !isForceSync { // allow first command to be sync with server if len(trackingData) < config.FlushCount && !noCursorExist { - logrus.Traceln("not enough data need to flush, abort. current is:", len(trackingData)) + slog.Debug("not enough data need to flush, abort", slog.Int("current", len(trackingData))) return nil } } err = DoSyncData(ctx, config, latestRecordingTime, trackingData, meta) if err != nil { - logrus.Errorln("Failed to send data to server:", err) + slog.Error("Failed to send data to server", slog.Any("err", err)) return err } @@ -267,7 +267,7 @@ func DoSyncData( socketPath := config.SocketPath isSocketReady := daemon.IsSocketReady(ctx, socketPath) - logrus.Traceln("is socket ready: ", isSocketReady) + slog.Debug("is socket ready", slog.Bool("ready", isSocketReady)) // if the socket not ready, just call http to sync data if !isSocketReady { @@ -285,17 +285,17 @@ func DoSyncData( func updateCursorToFile(ctx context.Context, latestRecordingTime time.Time) error { ctx, span := commandTracer.Start(ctx, "updateCurosr") defer span.End() - cursorFilePath := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", model.COMMAND_CURSOR_STORAGE_FILE)) + cursorFilePath := model.GetCursorFilePath() cursorFile, err := os.OpenFile(cursorFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { - logrus.Errorln("Failed to open cursor file for writing:", err) + slog.Error("Failed to open cursor file for writing", slog.Any("err", err)) return err } defer cursorFile.Close() _, err = cursorFile.WriteString(fmt.Sprintf("\n%d\n", latestRecordingTime.UnixNano())) if err != nil { - logrus.Errorln("Failed to write to cursor file:", err) + slog.Error("Failed to write to cursor file", slog.Any("err", err)) return err } return nil diff --git a/commands/track_test.go b/commands/track_test.go index f44f72c..a813744 100644 --- a/commands/track_test.go +++ b/commands/track_test.go @@ -17,7 +17,6 @@ import ( "time" "github.com/malamtime/cli/model" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" @@ -33,7 +32,6 @@ type trackTestSuite struct { // before each test func (s *trackTestSuite) SetupSuite() { - logrus.SetLevel(logrus.TraceLevel) s.baseTimeFolder = strconv.Itoa(int(time.Now().Unix())) otel.SetTracerProvider(noop.NewTracerProvider()) SKIP_LOGGER_SETTINGS = true @@ -263,7 +261,7 @@ func (s *trackTestSuite) TestTrackWithSendData() { // Check the cursor file should be only one line cursorContent, err = os.ReadFile(cursorFile) assert.Nil(s.T(), err) - logrus.Infoln(string(cursorContent)) + s.T().Log("cursor content:", string(cursorContent)) cursorLines := bytes.Split(cursorContent, []byte("\n")) assert.Len(s.T(), cursorLines, 1) @@ -271,7 +269,7 @@ func (s *trackTestSuite) TestTrackWithSendData() { preContent, err := os.ReadFile(preFile) assert.Nil(s.T(), err) preContent = bytes.TrimSpace(preContent) - logrus.Infoln(string(preContent)) + s.T().Log("pre content:", string(preContent)) preLines := bytes.Split(preContent, []byte("\n")) assert.LessOrEqual(s.T(), len(preLines)-1, times) assert.Contains(s.T(), string(preContent), "unfinished_cmd") @@ -280,7 +278,7 @@ func (s *trackTestSuite) TestTrackWithSendData() { postContent, err = os.ReadFile(postFile) assert.Nil(s.T(), err) postContent = bytes.TrimSpace(postContent) - logrus.Infoln(string(postContent)) + s.T().Log("post content:", string(postContent)) postBytesLines := bytes.Split(postContent, []byte("\n")) assert.Less(s.T(), len(postBytesLines), times) assert.NotContains(s.T(), string(postContent), "unfinished_cmd") diff --git a/commands/utils.go b/commands/utils.go index 5a4d808..a120896 100644 --- a/commands/utils.go +++ b/commands/utils.go @@ -2,11 +2,10 @@ package commands import ( "fmt" + "log/slog" "os" "path/filepath" "strings" - - "github.com/sirupsen/logrus" ) func expandPath(path string) (string, error) { @@ -24,7 +23,7 @@ func expandPath(path string) (string, error) { func AdjustPathForCurrentUser(path string) string { homeDir, err := os.UserHomeDir() if err != nil { - logrus.Warnf("Failed to get home directory: %v", err) + slog.Warn("Failed to get home directory", slog.Any("err", err)) return path } diff --git a/daemon/handlers.go b/daemon/handlers.go index 0f24f11..080f6fc 100644 --- a/daemon/handlers.go +++ b/daemon/handlers.go @@ -8,7 +8,7 @@ import ( "github.com/ThreeDotsLabs/watermill/message" ) -func SocketTopicProccessor(messages <-chan *message.Message) { +func SocketTopicProcessor(messages <-chan *message.Message) { for msg := range messages { ctx := context.Background() slog.InfoContext(ctx, "received message: ", slog.String("msg.uuid", msg.UUID)) diff --git a/daemon/handlers_test.go b/daemon/handlers_test.go index 9336a93..f1b2925 100644 --- a/daemon/handlers_test.go +++ b/daemon/handlers_test.go @@ -54,7 +54,7 @@ func (s *handlersTestSuite) TestSocketTopicProcessorValidSync() { msg := message.NewMessage("test-uuid", payload) - go SocketTopicProccessor(msgChan) + go SocketTopicProcessor(msgChan) msgChan <- msg @@ -68,7 +68,7 @@ func (s *handlersTestSuite) TestSocketTopicProcessorInvalidFormat() { msg := message.NewMessage("test-uuid", []byte("invalid")) - go SocketTopicProccessor(msgChan) + go SocketTopicProcessor(msgChan) msgChan <- msg @@ -96,7 +96,7 @@ func (s *handlersTestSuite) TestSocketTopicProcessorNonSync() { msg := message.NewMessage("test-uuid", payload) - go SocketTopicProccessor(msgChan) + go SocketTopicProcessor(msgChan) msgChan <- msg @@ -117,7 +117,7 @@ func (s *handlersTestSuite) TestSocketTopicProcessorInvalidPayload() { msg := message.NewMessage("test-uuid", payload) - go SocketTopicProccessor(msgChan) + go SocketTopicProcessor(msgChan) msgChan <- msg @@ -160,7 +160,7 @@ func (s *handlersTestSuite) TestSocketTopicProcessorMultipleMessages() { msg1 := message.NewMessage("test-uuid-1", payload1) msg2 := message.NewMessage("test-uuid-2", payload2) - go SocketTopicProccessor(msgChan) + go SocketTopicProcessor(msgChan) msgChan <- msg1 msgChan <- msg2 diff --git a/go.mod b/go.mod index 6b50e08..a3b84f1 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.82 github.com/sergi/go-diff v1.4.0 - github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.11.1 github.com/uptrace/uptrace-go v1.39.0 github.com/urfave/cli/v2 v2.27.7 @@ -25,7 +24,6 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 go.opentelemetry.io/proto/otlp v1.9.0 google.golang.org/grpc v1.77.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -85,4 +83,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4e69bd5..a548548 100644 --- a/go.sum +++ b/go.sum @@ -154,8 +154,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -239,7 +237,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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= diff --git a/model/alias.go b/model/alias.go index dfccc85..e8a9137 100644 --- a/model/alias.go +++ b/model/alias.go @@ -3,11 +3,10 @@ package model import ( "context" "fmt" + "log/slog" "net/http" "os" "os/user" - - "github.com/sirupsen/logrus" ) // Alias represents a shell alias @@ -37,13 +36,13 @@ type importShellAliasResponse struct { // SendAliasesToServer sends the collected aliases to the server func SendAliasesToServer(ctx context.Context, endpoint Endpoint, aliases []string, isFullyRefresh bool, shellType, fileLocation string) error { if len(aliases) == 0 { - logrus.Infoln("No aliases to send") + slog.Info("No aliases to send") return nil } sysInfo, err := GetOSAndVersion() if err != nil { - logrus.Warnln(err) + slog.Warn("failed to get OS version", slog.Any("err", err)) sysInfo = &SysInfo{ Os: "unknown", Version: "unknown", @@ -52,7 +51,7 @@ func SendAliasesToServer(ctx context.Context, endpoint Endpoint, aliases []strin hostname, err := os.Hostname() if err != nil { - logrus.Warnln("Failed to get hostname:", err) + slog.Warn("Failed to get hostname", slog.Any("err", err)) hostname = "unknown" } @@ -60,7 +59,7 @@ func SendAliasesToServer(ctx context.Context, endpoint Endpoint, aliases []strin if username == "" { currentUser, err := user.Current() if err != nil { - logrus.Warnln("Failed to get username:", err) + slog.Warn("Failed to get username", slog.Any("err", err)) username = "unknown" } else { username = currentUser.Username @@ -92,6 +91,6 @@ func SendAliasesToServer(ctx context.Context, endpoint Endpoint, aliases []strin return fmt.Errorf("failed to send aliases to server: %w", err) } - logrus.Infoln("save aliases successfully", resp.Count) + slog.Info("save aliases successfully", slog.Int("count", resp.Count)) return nil } diff --git a/model/api.base.go b/model/api.base.go index f98e307..e204006 100644 --- a/model/api.base.go +++ b/model/api.base.go @@ -7,10 +7,10 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "time" - "github.com/sirupsen/logrus" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -33,7 +33,7 @@ func SendHTTPRequestJSON[T any, R any](opts HTTPRequestOptions[T, R]) error { jsonData, err := json.Marshal(opts.Payload) if err != nil { - logrus.Errorln(err) + slog.Error("failed to marshal payload", slog.Any("err", err)) return err } @@ -49,7 +49,7 @@ func SendHTTPRequestJSON[T any, R any](opts HTTPRequestOptions[T, R]) error { req, err := http.NewRequestWithContext(ctx, opts.Method, opts.Endpoint.APIEndpoint+opts.Path, bytes.NewBuffer(jsonData)) if err != nil { - logrus.Errorln(err) + slog.Error("failed to create request", slog.Any("err", err)) return err } @@ -62,16 +62,16 @@ func SendHTTPRequestJSON[T any, R any](opts HTTPRequestOptions[T, R]) error { req.Header.Set("User-Agent", fmt.Sprintf("shelltimeCLI@%s", commitID)) req.Header.Set("Authorization", "CLI "+opts.Endpoint.Token) - logrus.Traceln("http: ", req.URL.String()) + slog.Debug("http request", slog.String("url", req.URL.String())) resp, err := client.Do(req) if err != nil { - logrus.Errorln(err) + slog.Error("failed to send request", slog.Any("err", err)) return err } defer resp.Body.Close() - logrus.Traceln("http: ", resp.Status) + slog.Debug("http response", slog.String("status", resp.Status)) if resp.StatusCode == http.StatusNoContent { return nil @@ -79,7 +79,7 @@ func SendHTTPRequestJSON[T any, R any](opts HTTPRequestOptions[T, R]) error { buf, err := io.ReadAll(resp.Body) if err != nil { - logrus.Errorln(err) + slog.Error("failed to read response body", slog.Any("err", err)) return err } @@ -87,10 +87,10 @@ func SendHTTPRequestJSON[T any, R any](opts HTTPRequestOptions[T, R]) error { var msg errorResponse err = json.Unmarshal(buf, &msg) if err != nil { - logrus.Errorln("Failed to parse error response:", err) + slog.Error("Failed to parse error response", slog.Any("err", err)) return fmt.Errorf("HTTP error: %d", resp.StatusCode) } - logrus.Errorln("Error response:", msg.ErrorMessage) + slog.Error("Error response", slog.String("message", msg.ErrorMessage)) return errors.New(msg.ErrorMessage) } @@ -98,7 +98,7 @@ func SendHTTPRequestJSON[T any, R any](opts HTTPRequestOptions[T, R]) error { if opts.Response != nil { err = json.Unmarshal(buf, opts.Response) if err != nil { - logrus.Errorln("Failed to unmarshal JSON response:", err) + slog.Error("Failed to unmarshal JSON response", slog.Any("err", err)) return err } } diff --git a/model/api.go b/model/api.go index 4776912..8266b47 100644 --- a/model/api.go +++ b/model/api.go @@ -2,10 +2,9 @@ package model import ( "context" + "log/slog" "net/http" "sync" - - "github.com/sirupsen/logrus" ) type errorResponse struct { @@ -56,10 +55,10 @@ func doSendData(ctx context.Context, endpoint Endpoint, data PostTrackArgs) erro Payload: data, Response: nil, }) - logrus.Traceln("http: ", "/api/v1/track", len(data.Data), data.Meta) + slog.Debug("http track request", slog.String("path", "/api/v1/track"), slog.Int("dataLen", len(data.Data))) if err != nil { - logrus.Errorln(err) + slog.Error("failed to send data", slog.Any("err", err)) return err } return nil @@ -70,7 +69,7 @@ func SendLocalDataToServer(ctx context.Context, config ShellTimeConfig, data Pos ctx, span := modelTracer.Start(ctx, "sync.local") defer span.End() if config.Token == "" { - logrus.Traceln("no token available. do not sync to server") + slog.Debug("no token available. do not sync to server") return nil } diff --git a/model/command.go b/model/command.go index 3ebc7cd..21055d5 100644 --- a/model/command.go +++ b/model/command.go @@ -4,12 +4,11 @@ import ( "bytes" "encoding/json" "fmt" + "log/slog" "os" "strconv" "strings" "time" - - "github.com/sirupsen/logrus" ) // pre commands here @@ -43,7 +42,7 @@ type Command struct { } func ensureStorageFolder() error { - storageFolder := os.ExpandEnv("$HOME/" + COMMAND_STORAGE_FOLDER) + storageFolder := GetCommandsStoragePath() if _, err := os.Stat(storageFolder); os.IsNotExist(err) { if err := os.MkdirAll(storageFolder, 0755); err != nil { return fmt.Errorf("failed to create command storage folder: %v", err) @@ -63,19 +62,17 @@ func (c Command) DoSavePre() error { return err } - preFile := os.ExpandEnv(os.ExpandEnv("$HOME/" + COMMAND_PRE_STORAGE_FILE)) + preFile := GetPreCommandFilePath() f, err := os.OpenFile(preFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { - err = fmt.Errorf("failed to open pre-command storage file: %v", err) - logrus.Errorln(err) - return err + slog.Error("failed to open pre-command storage file", slog.Any("err", err)) + return fmt.Errorf("failed to open pre-command storage file: %v", err) } defer f.Close() if _, err := f.Write(buf); err != nil { - err = fmt.Errorf("failed to write to pre-command storage file: %v", err) - logrus.Errorln(err) - return err + slog.Error("failed to write to pre-command storage file", slog.Any("err", err)) + return fmt.Errorf("failed to write to pre-command storage file: %v", err) } return nil } @@ -93,7 +90,7 @@ func (c Command) DoUpdate(result int) error { return err } - postFile := os.ExpandEnv("$HOME/" + COMMAND_POST_STORAGE_FILE) + postFile := GetPostCommandFilePath() f, err := os.OpenFile(postFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return fmt.Errorf("failed to open post-command storage file: %v", err) @@ -173,21 +170,21 @@ func (cmd *Command) FromLine(line string) (recordingTime time.Time, err error) { parts := strings.Split(line, string(SEPARATOR)) if len(parts) != 2 { err = fmt.Errorf("Invalid line format in pre-command file: %s", line) - logrus.Errorln(err) + slog.Error("invalid line format", slog.String("line", line)) return } err = json.Unmarshal([]byte(parts[0]), cmd) if err != nil { err = fmt.Errorf("failed to unmarshal command: %v, %s", err, parts[0]) - logrus.Errorln(err) + slog.Error("failed to unmarshal command", slog.Any("err", err)) return } unixNano, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { err = fmt.Errorf("failed to parse timestamp: %v, %s", err, parts[1]) - logrus.Errorln(err) + slog.Error("failed to parse timestamp", slog.Any("err", err)) return } recordingTime = time.Unix(0, unixNano) @@ -199,21 +196,21 @@ func (cmd *Command) FromLineBytes(line []byte) (recordingTime time.Time, err err parts := bytes.Split(line, []byte{SEPARATOR}) if len(parts) != 2 { err = fmt.Errorf("Invalid line format in FromLineBytes: %s", string(line)) - logrus.Errorln(err) + slog.Error("invalid line format", slog.String("line", string(line))) return } err = json.Unmarshal(parts[0], cmd) if err != nil { err = fmt.Errorf("failed to unmarshal command: %v, %s, %s", err, string(parts[0]), string(line)) - logrus.Errorln(err) + slog.Error("failed to unmarshal command", slog.Any("err", err)) return } unixNano, err := strconv.ParseInt(string(parts[1]), 10, 64) if err != nil { err = fmt.Errorf("failed to parse timestamp: %v, %s", err, string(parts[1])) - logrus.Errorln(err) + slog.Error("failed to parse timestamp", slog.Any("err", err)) return } recordingTime = time.Unix(0, unixNano) diff --git a/model/db.go b/model/db.go index 43d5ca1..d818122 100644 --- a/model/db.go +++ b/model/db.go @@ -6,11 +6,10 @@ import ( "context" "fmt" "io" + "log/slog" "os" "strconv" "time" - - "github.com/sirupsen/logrus" ) const ( @@ -47,10 +46,10 @@ type preCommandTree map[string][]*Command func GetPreCommandsTree(ctx context.Context) (result preCommandTree, err error) { ctx, span := modelTracer.Start(ctx, "db.getPreCmdsTree") defer span.End() - preFilePath := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", COMMAND_PRE_STORAGE_FILE)) + preFilePath := GetPreCommandFilePath() preFileHandler, err := os.Open(preFilePath) if err != nil { - logrus.Errorln("Failed to open pre-command file:", err) + slog.Error("Failed to open pre-command file", slog.Any("err", err)) return nil, err } defer preFileHandler.Close() @@ -64,7 +63,7 @@ func GetPreCommandsTree(ctx context.Context) (result preCommandTree, err error) cmd := new(Command) _, err := cmd.FromLineBytes(line) if err != nil { - logrus.Errorln("Invalid line parse in pre-command file:", line, err) + slog.Error("Invalid line parse in pre-command file", slog.String("line", string(line)), slog.Any("err", err)) continue } @@ -86,10 +85,10 @@ func GetPreCommandsTree(ctx context.Context) (result preCommandTree, err error) func GetPreCommands(ctx context.Context) ([]*Command, error) { ctx, span := modelTracer.Start(ctx, "db.getPreCmds") defer span.End() - preFilePath := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", COMMAND_PRE_STORAGE_FILE)) + preFilePath := GetPreCommandFilePath() preFileHandler, err := os.Open(preFilePath) if err != nil { - logrus.Errorln("Failed to open pre-command file:", err) + slog.Error("Failed to open pre-command file", slog.Any("err", err)) return nil, err } defer preFileHandler.Close() @@ -107,14 +106,14 @@ func GetPreCommands(ctx context.Context) ([]*Command, error) { cmd := new(Command) _, err := cmd.FromLineBytes(raw) if err != nil { - logrus.Errorln("Invalid line parse in pre-command file:", string(raw), err) + slog.Error("Invalid line parse in pre-command file", slog.String("line", string(raw)), slog.Any("err", err)) continue } result = append(result, cmd) } if err := scanner.Err(); err != nil { - logrus.Errorln("Error reading file:", err) + slog.Error("Error reading file", slog.Any("err", err)) return nil, err } @@ -125,7 +124,7 @@ func GetLastCursor(ctx context.Context) (cursorTime time.Time, noCursorExist boo ctx, span := modelTracer.Start(ctx, "db.getLastCursor") defer span.End() noCursorExist = false - cursorFilePath := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", COMMAND_CURSOR_STORAGE_FILE)) + cursorFilePath := GetCursorFilePath() cursorFile, err := os.Open(cursorFilePath) if err != nil { if os.IsNotExist(err) { @@ -134,7 +133,7 @@ func GetLastCursor(ctx context.Context) (cursorTime time.Time, noCursorExist boo err = nil return } - logrus.Errorln("Failed to open cursor file:", err) + slog.Error("Failed to open cursor file", slog.Any("err", err)) return } defer cursorFile.Close() @@ -142,7 +141,7 @@ func GetLastCursor(ctx context.Context) (cursorTime time.Time, noCursorExist boo fileContent, err := io.ReadAll(cursorFile) if err != nil { - logrus.Errorln("Error reading cursor file:", err) + slog.Error("Error reading cursor file", slog.Any("err", err)) return cursorTime, false, err } @@ -161,7 +160,7 @@ func GetLastCursor(ctx context.Context) (cursorTime time.Time, noCursorExist boo cursor, err := strconv.Atoi(lastLine) if err != nil { - logrus.Errorln("Failed to parse cursor value:", err) + slog.Error("Failed to parse cursor value", slog.Any("err", err)) return } cursorTime = time.Unix(0, int64(cursor)) @@ -171,10 +170,10 @@ func GetLastCursor(ctx context.Context) (cursorTime time.Time, noCursorExist boo func GetPostCommands(ctx context.Context) ([][]byte, int, error) { ctx, span := modelTracer.Start(ctx, "db.getPostCmds") defer span.End() - postFilePath := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", COMMAND_POST_STORAGE_FILE)) + postFilePath := GetPostCommandFilePath() postFileHandler, err := os.Open(postFilePath) if err != nil { - logrus.Errorln("Failed to open file:", err) + slog.Error("Failed to open file", slog.Any("err", err)) return nil, 0, err } defer postFileHandler.Close() @@ -192,7 +191,7 @@ func GetPostCommands(ctx context.Context) ([][]byte, int, error) { } if err := scanner.Err(); err != nil { - logrus.Errorln("Error reading file:", err) + slog.Error("Error reading file", slog.Any("err", err)) return nil, 0, err } lineCount := len(nonEmptyContent) diff --git a/model/dotfile.go b/model/dotfile.go index 1aa50f2..8f790f1 100644 --- a/model/dotfile.go +++ b/model/dotfile.go @@ -3,11 +3,10 @@ package model import ( "context" "fmt" + "log/slog" "net/http" "os" "time" - - "github.com/sirupsen/logrus" ) // DotfileItem represents a single dotfile to be sent to the server @@ -49,14 +48,14 @@ type dotfilePushResponse struct { // SendDotfilesToServer sends the collected dotfiles to the server func SendDotfilesToServer(ctx context.Context, endpoint Endpoint, dotfiles []DotfileItem) (int, error) { if len(dotfiles) == 0 { - logrus.Infoln("No dotfiles to send") + slog.Info("No dotfiles to send") return 0, nil } // Get system info for hostname hostname, err := os.Hostname() if err != nil { - logrus.Warnln("Failed to get hostname:", err) + slog.Warn("Failed to get hostname", slog.Any("err", err)) hostname = "unknown" } @@ -85,12 +84,12 @@ func SendDotfilesToServer(ctx context.Context, endpoint Endpoint, dotfiles []Dot return 0, fmt.Errorf("failed to send dotfiles to server: %w", err) } - logrus.Infof("Pushed dotfiles successfully - Success: %d, Failed: %d", resp.Success, resp.Failed) + slog.Info("Pushed dotfiles successfully", slog.Int("success", resp.Success), slog.Int("failed", resp.Failed)) // Log any errors for _, result := range resp.Results { if result.Status == "error" { - logrus.Warnf("Error pushing %s (%s): %s", result.App, result.Path, result.Error) + slog.Warn("Error pushing dotfile", slog.String("app", result.App), slog.String("path", result.Path), slog.String("error", result.Error)) } } diff --git a/model/dotfile_apps.go b/model/dotfile_apps.go index 7434cb1..43f4bf1 100644 --- a/model/dotfile_apps.go +++ b/model/dotfile_apps.go @@ -4,12 +4,11 @@ import ( "context" "crypto/sha256" "fmt" + "log/slog" "os" "path/filepath" "strings" "time" - - "github.com/sirupsen/logrus" ) type DotfileAppName string @@ -122,14 +121,14 @@ func (b *BaseApp) CollectFromPaths(_ context.Context, appName string, paths []st for _, path := range paths { expandedPath, err := b.expandPath(path) if err != nil { - logrus.Debugf("Failed to expand path %s: %v", path, err) + slog.Debug("Failed to expand path", slog.String("path", path), slog.Any("err", err)) continue } // Check if it's a directory or file fileInfo, err := os.Stat(expandedPath) if err != nil { - logrus.Debugf("Path not found: %s", expandedPath) + slog.Debug("Path not found", slog.String("path", expandedPath)) continue } @@ -137,14 +136,14 @@ func (b *BaseApp) CollectFromPaths(_ context.Context, appName string, paths []st // For directories, collect specific files files, err := b.collectFromDirectory(expandedPath) if err != nil { - logrus.Debugf("Failed to collect from directory %s: %v", expandedPath, err) + slog.Debug("Failed to collect from directory", slog.String("path", expandedPath), slog.Any("err", err)) continue } for _, file := range files { content, modTime, err := b.readFileContent(file) if err != nil { - logrus.Debugf("Failed to read file %s: %v", file, err) + slog.Debug("Failed to read file", slog.String("file", file), slog.Any("err", err)) continue } @@ -166,7 +165,7 @@ func (b *BaseApp) CollectFromPaths(_ context.Context, appName string, paths []st // Single file content, modTime, err := b.readFileContent(expandedPath) if err != nil { - logrus.Debugf("Failed to read file %s: %v", expandedPath, err) + slog.Debug("Failed to read file", slog.String("path", expandedPath), slog.Any("err", err)) continue } @@ -236,7 +235,7 @@ func (b *BaseApp) IsEqual(_ context.Context, files map[string]string) (map[strin for path, remoteContent := range files { expandedPath, err := b.expandPath(path) if err != nil { - logrus.Debugf("Failed to expand path %s: %v", path, err) + slog.Debug("Failed to expand path", slog.String("path", path), slog.Any("err", err)) result[path] = false continue } @@ -248,7 +247,7 @@ func (b *BaseApp) IsEqual(_ context.Context, files map[string]string) (map[strin // File doesn't exist locally, so it's not equal result[path] = false } else { - logrus.Debugf("Failed to read file %s: %v", expandedPath, err) + slog.Debug("Failed to read file", slog.String("path", expandedPath), slog.Any("err", err)) result[path] = false } continue @@ -271,14 +270,14 @@ func (b *BaseApp) Backup(ctx context.Context, paths []string, isDryRun bool) err for _, path := range paths { expandedPath, err := b.expandPath(path) if err != nil { - logrus.Warnf("Failed to expand path %s: %v", path, err) + slog.Warn("Failed to expand path", slog.String("path", path), slog.Any("err", err)) continue } // Check if file exists if _, err := os.Stat(expandedPath); err != nil { if !os.IsNotExist(err) { - logrus.Warnf("Failed to stat file %s: %v", expandedPath, err) + slog.Warn("Failed to stat file", slog.String("path", expandedPath), slog.Any("err", err)) } continue // Skip if file doesn't exist } @@ -287,17 +286,17 @@ func (b *BaseApp) Backup(ctx context.Context, paths []string, isDryRun bool) err backupPath := expandedPath + ".backup." + time.Now().Format("20060102-150405") existingContent, err := os.ReadFile(expandedPath) if err != nil { - logrus.Warnf("Failed to read existing file for backup: %v", err) + slog.Warn("Failed to read existing file for backup", slog.Any("err", err)) continue } if isDryRun { - logrus.Infof("[DRY RUN] Would create backup for %s", expandedPath) + slog.Info("[DRY RUN] Would create backup", slog.String("path", expandedPath)) } else { if err := os.WriteFile(backupPath, existingContent, 0644); err != nil { - logrus.Warnf("Failed to create backup at %s: %v", backupPath, err) + slog.Warn("Failed to create backup", slog.String("backupPath", backupPath), slog.Any("err", err)) } else { - logrus.Infof("Created backup at %s", backupPath) + slog.Info("Created backup", slog.String("backupPath", backupPath)) } } } @@ -312,7 +311,7 @@ func (b *BaseApp) Save(ctx context.Context, files map[string]string, isDryRun bo for path, newContent := range files { expandedPath, err := b.expandPath(path) if err != nil { - logrus.Warnf("Failed to expand path %s: %v", path, err) + slog.Warn("Failed to expand path", slog.String("path", path), slog.Any("err", err)) continue } @@ -321,7 +320,7 @@ func (b *BaseApp) Save(ctx context.Context, files map[string]string, isDryRun bo if existingBytes, err := os.ReadFile(expandedPath); err == nil { existingContent = string(existingBytes) } else if !os.IsNotExist(err) { - logrus.Warnf("Failed to read existing file %s: %v", expandedPath, err) + slog.Warn("Failed to read existing file", slog.String("path", expandedPath), slog.Any("err", err)) continue } @@ -362,15 +361,15 @@ func (b *BaseApp) Save(ctx context.Context, files map[string]string, isDryRun bo // Ensure directory exists dir := filepath.Dir(expandedPath) if err := os.MkdirAll(dir, 0755); err != nil { - logrus.Warnf("Failed to create directory %s: %v", dir, err) + slog.Warn("Failed to create directory", slog.String("dir", dir), slog.Any("err", err)) continue } // Write merged content if err := os.WriteFile(expandedPath, mergedContent, 0644); err != nil { - logrus.Warnf("Failed to save file %s: %v", expandedPath, err) + slog.Warn("Failed to save file", slog.String("path", expandedPath), slog.Any("err", err)) } else { - logrus.Infof("Saved new content to %s", expandedPath) + slog.Info("Saved new content", slog.String("path", expandedPath)) } } diff --git a/model/dotfile_ghostty.go b/model/dotfile_ghostty.go index 0506acd..e6bf480 100644 --- a/model/dotfile_ghostty.go +++ b/model/dotfile_ghostty.go @@ -4,11 +4,10 @@ import ( "bufio" "context" "fmt" + "log/slog" "os" "path/filepath" "strings" - - "github.com/sirupsen/logrus" ) // GhosttyApp handles Ghostty terminal configuration files @@ -131,7 +130,7 @@ func (g *GhosttyApp) Save(ctx context.Context, files map[string]string, isDryRun for path, remoteContent := range files { expandedPath, err := g.expandPath(path) if err != nil { - logrus.Warnf("Failed to expand path %s: %v", path, err) + slog.Warn("Failed to expand path", slog.String("path", path), slog.Any("err", err)) continue } @@ -140,7 +139,7 @@ func (g *GhosttyApp) Save(ctx context.Context, files map[string]string, isDryRun if existingBytes, err := os.ReadFile(expandedPath); err == nil { localContent = string(existingBytes) } else if !os.IsNotExist(err) { - logrus.Warnf("Failed to read existing file %s: %v", expandedPath, err) + slog.Warn("Failed to read existing file", slog.String("path", expandedPath), slog.Any("err", err)) continue } @@ -154,7 +153,7 @@ func (g *GhosttyApp) Save(ctx context.Context, files map[string]string, isDryRun // Check if there are any differences if localContent == mergedContent { - logrus.Infof("No changes needed for %s", expandedPath) + slog.Info("No changes needed", slog.String("path", expandedPath)) continue } @@ -198,15 +197,15 @@ func (g *GhosttyApp) Save(ctx context.Context, files map[string]string, isDryRun // Ensure directory exists dir := filepath.Dir(expandedPath) if err := os.MkdirAll(dir, 0755); err != nil { - logrus.Warnf("Failed to create directory %s: %v", dir, err) + slog.Warn("Failed to create directory", slog.String("dir", dir), slog.Any("err", err)) continue } // Write merged content if err := os.WriteFile(expandedPath, []byte(mergedContent), 0644); err != nil { - logrus.Warnf("Failed to save file %s: %v", expandedPath, err) + slog.Warn("Failed to save file", slog.String("path", expandedPath), slog.Any("err", err)) } else { - logrus.Infof("Saved merged config to %s", expandedPath) + slog.Info("Saved merged config", slog.String("path", expandedPath)) } } diff --git a/model/exclude.go b/model/exclude.go index 5c5d24f..3b8deb8 100644 --- a/model/exclude.go +++ b/model/exclude.go @@ -1,9 +1,8 @@ package model import ( + "log/slog" "regexp" - - "github.com/sirupsen/logrus" ) // ShouldExcludeCommand checks if a command matches any of the exclude patterns @@ -19,12 +18,12 @@ func ShouldExcludeCommand(command string, excludePatterns []string) bool { re, err := regexp.Compile(pattern) if err != nil { - logrus.Warnf("Invalid exclude pattern '%s': %v", pattern, err) + slog.Warn("Invalid exclude pattern", slog.String("pattern", pattern), slog.Any("err", err)) continue } if re.MatchString(command) { - logrus.Tracef("Command '%s' matches exclude pattern '%s'", command, pattern) + slog.Debug("Command matches exclude pattern", slog.String("command", command), slog.String("pattern", pattern)) return true } } diff --git a/model/handshake.go b/model/handshake.go index 7015753..38f0cba 100644 --- a/model/handshake.go +++ b/model/handshake.go @@ -7,11 +7,11 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "os" "time" - "github.com/sirupsen/logrus" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -46,7 +46,7 @@ func (hs handshakeService) send(ctx context.Context, path string, jsonData []byt req, err := http.NewRequestWithContext(ctx, "POST", hs.config.APIEndpoint+"/api/v1/handshake"+path, bytes.NewBuffer(jsonData)) if err != nil { - logrus.Errorln(err) + slog.Error("failed to create request", slog.Any("err", err)) return } @@ -54,15 +54,15 @@ func (hs handshakeService) send(ctx context.Context, path string, jsonData []byt req.Header.Set("User-Agent", fmt.Sprintf("shelltimeCLI@%s", commitID)) resp, err := hc.Do(req) if err != nil { - logrus.Errorln(err) + slog.Error("failed to send request", slog.Any("err", err)) return } defer resp.Body.Close() - logrus.Traceln("http: ", resp.Status) + slog.Debug("http response", slog.String("status", resp.Status)) buf, err := io.ReadAll(resp.Body) if err != nil { - logrus.Errorln(err) + slog.Error("failed to read response body", slog.Any("err", err)) return } @@ -84,7 +84,7 @@ type handshakeInitRequest struct { func (hs handshakeService) Init(ctx context.Context) (string, error) { sysInfo, err := GetOSAndVersion() if err != nil { - logrus.Errorln(err) + slog.Error("failed to get OS version", slog.Any("err", err)) sysInfo = &SysInfo{ Os: "unknown", Version: "unknown", @@ -104,7 +104,7 @@ func (hs handshakeService) Init(ctx context.Context) (string, error) { jsonData, err := json.Marshal(data) if err != nil { - logrus.Errorln(err) + slog.Error("failed to marshal handshake init request", slog.Any("err", err)) return "", err } @@ -131,7 +131,7 @@ func (hs handshakeService) Check(ctx context.Context, handshakeId string) (token jsonData, err := json.Marshal(data) if err != nil { - logrus.Errorln(err) + slog.Error("failed to marshal handshake check request", slog.Any("err", err)) return "", err } diff --git a/model/path.go b/model/path.go new file mode 100644 index 0000000..5268f63 --- /dev/null +++ b/model/path.go @@ -0,0 +1,74 @@ +package model + +import ( + "fmt" + "os" + "path/filepath" +) + +// GetBaseStoragePath returns the base storage path for shelltime +// e.g., /Users/username/.shelltime +func GetBaseStoragePath() string { + return os.ExpandEnv("$HOME/" + COMMAND_BASE_STORAGE_FOLDER) +} + +// GetStoragePath returns the full path for a given subpath within the storage folder +// e.g., GetStoragePath("config.toml") returns /Users/username/.shelltime/config.toml +func GetStoragePath(subpath string) string { + return filepath.Join(GetBaseStoragePath(), subpath) +} + +// GetConfigFilePath returns the path to the main config file +func GetConfigFilePath() string { + return GetStoragePath("config.toml") +} + +// GetLocalConfigFilePath returns the path to the local config file +func GetLocalConfigFilePath() string { + return GetStoragePath("config.local.toml") +} + +// GetLogFilePath returns the path to the log file +func GetLogFilePath() string { + return GetStoragePath("log.log") +} + +// GetCommandsStoragePath returns the path to the commands storage folder +func GetCommandsStoragePath() string { + return os.ExpandEnv("$HOME/" + COMMAND_STORAGE_FOLDER) +} + +// GetPreCommandFilePath returns the path to the pre-command storage file +func GetPreCommandFilePath() string { + return os.ExpandEnv("$HOME/" + COMMAND_PRE_STORAGE_FILE) +} + +// GetPostCommandFilePath returns the path to the post-command storage file +func GetPostCommandFilePath() string { + return os.ExpandEnv("$HOME/" + COMMAND_POST_STORAGE_FILE) +} + +// GetCursorFilePath returns the path to the cursor storage file +func GetCursorFilePath() string { + return os.ExpandEnv("$HOME/" + COMMAND_CURSOR_STORAGE_FILE) +} + +// GetHeartbeatLogFilePath returns the path to the heartbeat log file +func GetHeartbeatLogFilePath() string { + return os.ExpandEnv("$HOME/" + HEARTBEAT_LOG_FILE) +} + +// GetSyncPendingFilePath returns the path to the sync pending file +func GetSyncPendingFilePath() string { + return os.ExpandEnv("$HOME/" + SYNC_PENDING_FILE) +} + +// GetBinFolderPath returns the path to the bin folder +func GetBinFolderPath() string { + return os.ExpandEnv(fmt.Sprintf("$HOME/%s/bin", COMMAND_BASE_STORAGE_FOLDER)) +} + +// GetHooksFolderPath returns the path to the hooks folder +func GetHooksFolderPath() string { + return os.ExpandEnv(fmt.Sprintf("$HOME/%s/hooks", COMMAND_BASE_STORAGE_FOLDER)) +} From b6e8bf6f4ed5c045255a8eb931a9c2d2b5043df1 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 25 Dec 2025 13:34:21 +0000 Subject: [PATCH 2/2] refactor(model): use filepath.Join for cross-platform path handling Replace hardcoded '/' separators and os.ExpandEnv("$HOME/...") with filepath.Join and os.UserHomeDir() for Windows compatibility. - Use os.UserHomeDir() instead of $HOME env var - Add fallback to os.TempDir() when home dir unavailable - Make GetStoragePath variadic for flexible path construction - All path helpers now use filepath.Join for portability Co-authored-by: Le He --- model/path.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/model/path.go b/model/path.go index 5268f63..ac16ccc 100644 --- a/model/path.go +++ b/model/path.go @@ -1,7 +1,6 @@ package model import ( - "fmt" "os" "path/filepath" ) @@ -9,13 +8,18 @@ import ( // GetBaseStoragePath returns the base storage path for shelltime // e.g., /Users/username/.shelltime func GetBaseStoragePath() string { - return os.ExpandEnv("$HOME/" + COMMAND_BASE_STORAGE_FOLDER) + home, err := os.UserHomeDir() + if err != nil { + // Fallback for environments where home dir is not available. + return filepath.Join(os.TempDir(), COMMAND_BASE_STORAGE_FOLDER) + } + return filepath.Join(home, COMMAND_BASE_STORAGE_FOLDER) } // GetStoragePath returns the full path for a given subpath within the storage folder // e.g., GetStoragePath("config.toml") returns /Users/username/.shelltime/config.toml -func GetStoragePath(subpath string) string { - return filepath.Join(GetBaseStoragePath(), subpath) +func GetStoragePath(subpaths ...string) string { + return filepath.Join(append([]string{GetBaseStoragePath()}, subpaths...)...) } // GetConfigFilePath returns the path to the main config file @@ -35,40 +39,40 @@ func GetLogFilePath() string { // GetCommandsStoragePath returns the path to the commands storage folder func GetCommandsStoragePath() string { - return os.ExpandEnv("$HOME/" + COMMAND_STORAGE_FOLDER) + return GetStoragePath("commands") } // GetPreCommandFilePath returns the path to the pre-command storage file func GetPreCommandFilePath() string { - return os.ExpandEnv("$HOME/" + COMMAND_PRE_STORAGE_FILE) + return GetStoragePath("commands", "pre.txt") } // GetPostCommandFilePath returns the path to the post-command storage file func GetPostCommandFilePath() string { - return os.ExpandEnv("$HOME/" + COMMAND_POST_STORAGE_FILE) + return GetStoragePath("commands", "post.txt") } // GetCursorFilePath returns the path to the cursor storage file func GetCursorFilePath() string { - return os.ExpandEnv("$HOME/" + COMMAND_CURSOR_STORAGE_FILE) + return GetStoragePath("commands", "cursor.txt") } // GetHeartbeatLogFilePath returns the path to the heartbeat log file func GetHeartbeatLogFilePath() string { - return os.ExpandEnv("$HOME/" + HEARTBEAT_LOG_FILE) + return GetStoragePath("coding-heartbeat.data.log") } // GetSyncPendingFilePath returns the path to the sync pending file func GetSyncPendingFilePath() string { - return os.ExpandEnv("$HOME/" + SYNC_PENDING_FILE) + return GetStoragePath("sync-pending.jsonl") } // GetBinFolderPath returns the path to the bin folder func GetBinFolderPath() string { - return os.ExpandEnv(fmt.Sprintf("$HOME/%s/bin", COMMAND_BASE_STORAGE_FOLDER)) + return GetStoragePath("bin") } // GetHooksFolderPath returns the path to the hooks folder func GetHooksFolderPath() string { - return os.ExpandEnv(fmt.Sprintf("$HOME/%s/hooks", COMMAND_BASE_STORAGE_FOLDER)) + return GetStoragePath("hooks") }