diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b624ab1..611fd31 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,8 @@ name: Test on: - push: - branches: [ main ] + # push: + # branches: [ main ] pull_request: branches: [ main ] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a2d9bf2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,119 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +tmpo is a minimal CLI time tracker for developers built with Go. It uses Cobra for CLI commands, SQLite for local storage, and supports automatic project detection via Git or `.tmporc` configuration files. + +## Build and Development Commands + +**Build the project:** +```bash +go build -o tmpo . +``` + +**Run tests:** +```bash +go test -v ./... +``` + +**Test the binary:** +```bash +./tmpo --version +./tmpo --help +``` + +**Run locally without building:** +```bash +go run main.go [command] +``` + +**Test release build locally:** +```bash +goreleaser build --snapshot --clean +``` + +## Architecture + +### Core Components + +**CLI Layer** (`cmd/`): +- Uses Cobra for command structure +- Each command is a separate file (start.go, stop.go, status.go, etc.) +- All commands registered in `cmd/root.go` via `init()` functions +- Version information is injected via ldflags during build + +**Storage Layer** (`internal/storage/`): +- `db.go`: Database wrapper around `*sql.DB` with all query methods +- `models.go`: TimeEntry struct with Duration() and IsRunning() helper methods +- Uses modernc.org/sqlite (pure Go implementation) +- Database location: `$HOME/.tmpo/tmpo.db` +- Schema: time_entries table with id, project_name, start_time, end_time, description, hourly_rate + +**Configuration** (`internal/config/`): +- YAML-based config using `.tmporc` files +- Config fields: project_name, hourly_rate, description +- FindAndLoad() searches upward through parent directories for `.tmporc` +- Supports per-project configuration by placing `.tmporc` in project root + +**Project Detection** (`internal/project/`): +- Three-tier detection strategy: + 1. `.tmporc` file (highest priority) + 2. Git repository name via `git rev-parse --show-toplevel` + 3. Current directory name (fallback) +- Helper functions: FindTmporc(), GetGitRepoName(), IsInGitRepo(), GetGitRoot() + +**Export** (`internal/export/`): +- `csv.go`: Export to CSV format +- `json.go`: Export to JSON format +- Used by `export` command with filtering options + +### Key Patterns + +**Database Initialization:** +Every command that accesses the database follows this pattern: +```go +db, err := storage.Initialize() +if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) +} +defer db.Close() +``` + +**Project Name Detection:** +The `cmd/start.go:DetectProjectName()` function implements the priority: .tmporc config → Git repository → directory name + +**Time Entry States:** +- Running: EndTime is nil +- Stopped: EndTime is non-nil +- Methods: TimeEntry.IsRunning(), TimeEntry.Duration() + +**Hourly Rate Handling:** +HourlyRate is optional (*float64). Stored as sql.NullFloat64 in database queries and converted to/from pointer for the TimeEntry struct. + +**Config Template Pattern:** +The `.tmporc` file generation uses a template-based approach to ensure all fields are visible to users: +- Template is defined as `configTemplate` constant in `internal/config/config.go` +- Located directly below the `Config` struct for easy maintenance +- When adding new fields to `Config`, update both the struct AND the template (marked with IMPORTANT comments) + +## Important Notes + +**SQLite Driver:** +Uses `modernc.org/sqlite` (pure Go, no CGO) instead of mattn/go-sqlite3. This is important for cross-compilation. macOS builds keep CGO enabled, but Linux/Windows disable it (CGO_ENABLED=0). + +**Version Injection:** +Version, Commit, and Date are injected at build time via ldflags: +``` +-X github.com/DylanDevelops/tmpo/cmd.Version={{.Version}} +-X github.com/DylanDevelops/tmpo/cmd.Commit={{.Commit}} +-X github.com/DylanDevelops/tmpo/cmd.Date={{.Date}} +``` + +**Command Registration:** +New commands must be added via `rootCmd.AddCommand()` in their `init()` function. + +**Interactive Prompts:** +The `manual` command uses `github.com/manifoldco/promptui` for interactive prompts (date/time input). diff --git a/cmd/init.go b/cmd/init.go index 90719f8..90019f6 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -48,7 +48,7 @@ var initCmd = &cobra.Command{ } } - err := config.Create(name, hourlyRate) + err := config.CreateWithTemplate(name, hourlyRate) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/internal/config/config.go b/internal/config/config.go index eb5db0a..a78b73d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,12 +14,35 @@ import ( // ProjectName is the human-readable name of the project. // HourlyRate is the billable hourly rate for the project; when zero it will be omitted from YAML. // Description is an optional free-form description of the project; when empty it will be omitted from YAML. +// +// ! IMPORTANT When adding new fields to this struct, also update configTemplate below. ! type Config struct { ProjectName string `yaml:"project_name"` HourlyRate float64 `yaml:"hourly_rate,omitempty"` Description string `yaml:"description,omitempty"` } +// configTemplate is the template used when creating new .tmporc files via CreateWithTemplate. +// It includes all available configuration options with helpful comments. +// +// Format placeholders: +// %s - project name (string) +// %.2f - hourly rate (float64, 2 decimal places) +// +// ! IMPORTANT: When adding new fields to the Config struct above, update this template. ! +const configTemplate = `# tmpo project configuration +# This file configures time tracking settings for this project + +# Project name (used to identify time entries) +project_name: %s + +# [OPTIONAL] Hourly rate for billing calculations (set to 0 to disable) +hourly_rate: %.2f + +# [OPTIONAL] Description for this project +description: "" +` + // Load reads a YAML configuration file from the provided path and unmarshals it into a Config. // It returns a pointer to the populated Config on success. If the file cannot be read or the // contents cannot be parsed as YAML, Load returns a wrapped error describing the failure. @@ -72,6 +95,24 @@ func Create(projectName string, hourlyRate float64) error { return config.Save(tmporc) } +// CreateWithTemplate creates a new .tmporc file with a user-friendly format that includes +// all fields (even if empty) and helpful comments. This provides a better user experience +// by showing all available configuration options. +func CreateWithTemplate(projectName string, hourlyRate float64) error { + tmporc := filepath.Join(".", ".tmporc") + if _, err := os.Stat(tmporc); err == nil { + return fmt.Errorf(".tmporc already exists") + } + + content := fmt.Sprintf(configTemplate, projectName, hourlyRate) + + if err := os.WriteFile(tmporc, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write config: %w", err) + } + + return nil +} + // FindAndLoad searches upward from the current working directory for a file named // ".tmporc". Starting at os.Getwd(), it ascends parent directories until it either // finds the file or reaches the filesystem root.