diff --git a/SECURITY.md b/SECURITY.md index 8eca5eb..e0f4d43 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,7 +19,14 @@ tmpo is a local-first CLI tool that stores time tracking data on your machine. H - All time entries are stored in a SQLite database at `$HOME/.tmpo/tmpo.db` - The database is only accessible to your user account (standard file permissions apply) -- No data is transmitted over the network +- Your time tracking data is never transmitted over the network + +### **Update Checks** + +- When you run `tmpo version`, tmpo performs a check for newer releases by making a read-only request to the GitHub API (`https://api.github.com/repos/DylanDevelops/tmpo/releases/latest`) +- Only the latest release metadata is fetched; **no time tracking data, configuration, or personal information is sent** +- The request fails silently if you are offline, so it never blocks or interrupts your workflow +- You can disable this network request entirely by setting the `TMPO_NO_UPDATE_CHECK` environment variable to `1`, `true`, or `yes` ### **Configuration Files** diff --git a/cmd/history/stats.go b/cmd/history/stats.go index 15b6540..d15f895 100644 --- a/cmd/history/stats.go +++ b/cmd/history/stats.go @@ -16,6 +16,7 @@ import ( var ( statsToday bool statsWeek bool + statsMonth bool statsDate string ) @@ -63,6 +64,11 @@ func StatsCmd() *cobra.Command { start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).UTC().AddDate(0, 0, -weekday+1) end = start.AddDate(0, 0, 7) periodName = "This Week" + } else if statsMonth { + now := time.Now() + start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).UTC() + end = start.AddDate(0, 1, 0) + periodName = "This Month" } else { entries, err := db.GetEntries(0) if err != nil { @@ -86,6 +92,7 @@ func StatsCmd() *cobra.Command { cmd.Flags().BoolVarP(&statsToday, "today", "t", false, "Show today's stats") cmd.Flags().BoolVarP(&statsWeek, "week", "w", false, "Show this week's stats") + cmd.Flags().BoolVarP(&statsMonth, "month", "m", false, "Show this month's stats") cmd.Flags().StringVarP(&statsDate, "date", "d", "", "Show stats for a specific date") return cmd diff --git a/cmd/utilities/version.go b/cmd/utilities/version.go index 44a1407..25c0b07 100644 --- a/cmd/utilities/version.go +++ b/cmd/utilities/version.go @@ -2,6 +2,7 @@ package utilities import ( "fmt" + "os" "regexp" "strings" "time" @@ -65,14 +66,16 @@ func GetChangelogUrl(version string) string { } func checkForUpdates() { - // Only check for released semantic versions. + if isUpdateCheckDisabled() { + return + } + if !isReleaseVersion(Version) { return } updateInfo, err := update.CheckForUpdate(Version) if err != nil { - // Silently fail and don't bother the user with network errors return } @@ -86,3 +89,12 @@ func checkForUpdates() { func isReleaseVersion(version string) bool { return releaseVersionRegex.MatchString(version) } + +func isUpdateCheckDisabled() bool { + switch strings.ToLower(os.Getenv("TMPO_NO_UPDATE_CHECK")) { + case "1", "true", "yes": + return true + default: + return false + } +} diff --git a/cmd/utilities/version_test.go b/cmd/utilities/version_test.go index 6759cf6..d5f0335 100644 --- a/cmd/utilities/version_test.go +++ b/cmd/utilities/version_test.go @@ -1,11 +1,85 @@ package utilities import ( + "os" "testing" "github.com/stretchr/testify/assert" ) +func TestIsUpdateCheckDisabled(t *testing.T) { + tests := []struct { + name string + envValue string + setEnv bool + expected bool + }{ + { + name: "unset variable is enabled", + setEnv: false, + expected: false, + }, + { + name: "empty value is enabled", + envValue: "", + setEnv: true, + expected: false, + }, + { + name: "1 disables", + envValue: "1", + setEnv: true, + expected: true, + }, + { + name: "true disables", + envValue: "true", + setEnv: true, + expected: true, + }, + { + name: "yes disables", + envValue: "yes", + setEnv: true, + expected: true, + }, + { + name: "uppercase TRUE disables", + envValue: "TRUE", + setEnv: true, + expected: true, + }, + { + name: "0 does not disable", + envValue: "0", + setEnv: true, + expected: false, + }, + { + name: "arbitrary value does not disable", + envValue: "maybe", + setEnv: true, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setEnv { + t.Setenv("TMPO_NO_UPDATE_CHECK", tt.envValue) + } else { + // Ensure a deterministic "unset" state regardless of the + // ambient environment. t.Setenv registers cleanup that + // restores the original value after the subtest. + t.Setenv("TMPO_NO_UPDATE_CHECK", "placeholder") + os.Unsetenv("TMPO_NO_UPDATE_CHECK") + } + result := isUpdateCheckDisabled() + assert.Equal(t, tt.expected, result) + }) + } +} + func TestGetFormattedDate(t *testing.T) { tests := []struct { name string diff --git a/docs/usage.md b/docs/usage.md index 74736ee..dcd73f8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -99,7 +99,7 @@ View your time tracking history. **Options:** -- `--limit N` - Show N most recent entries (default: 20) +- `--limit N` - Show N most recent entries (default: 10) - `--milestone "name"` - Filter entries by milestone name - `--project "name"` - Filter entries by project name - `--today` - Show only today's entries @@ -135,6 +135,7 @@ Display statistics about your tracked time. tmpo stats # All-time stats tmpo stats --today # Today's stats tmpo stats --week # This week's stats +tmpo stats --month # This month's stats tmpo stats --date "2026-01-15" # January 15th, 2026 entries ```