diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cfc8d55 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build and Development Commands + +```bash +# Build the project +go build -o redmine + +# Install dependencies and clean up +go mod tidy + +# Test the built binary +./redmine --help + +# Run all tests (currently no tests exist) +go test -v ./... +``` + +## Project Architecture + +This is a Redmine CLI tool written in Go using the Cobra framework. The application follows a standard CLI architecture with clear separation of concerns: + +### Core Components + +- **main.go**: Entry point that delegates to cmd.Execute() +- **cmd/**: Contains all CLI command definitions using Cobra framework + - **root.go**: Root command setup with global profile flag + - **issues.go**: Issue management commands (list, show) + - **profile.go**: Profile management commands (add, list, use, remove, show) + - **auth.go**: Authentication commands (legacy, prefer profile commands) +- **config/**: Configuration management with YAML persistence + - **config.go**: Profile-based configuration with ~/.redminecli/config storage +- **client/**: Redmine API client with HTTP communication + - **client.go**: HTTP client with complete Redmine API models (Issue, Project, User, etc.) + +### Configuration System + +The application uses a profile-based configuration system: +- Configuration stored in `~/.redminecli/config` as YAML +- Each profile contains: name, Redmine URL, and API key +- Supports default profile and per-command profile override via `--profile` flag +- Profile management through `profile` commands (add, remove, use, list, show) + +### API Client Architecture + +The HTTP client (`client/client.go`) provides: +- Structured Redmine API models (Issue, Journal, Project, User, etc.) +- Authentication via X-Redmine-API-Key header +- JSON response parsing with proper error handling +- Support for pagination and filtering parameters +- Include parameters for fetching related data (e.g., journals for comments) + +### Command Structure + +Commands follow a hierarchical structure: +- `redmine issues list` - List issues with filtering options +- `redmine issues show ` - Show issue details with optional comments +- `redmine profile add/list/use/remove/show` - Profile management +- Global `--profile` flag for per-command profile selection + +### Dependencies + +- **github.com/spf13/cobra**: CLI framework for command structure +- **gopkg.in/yaml.v3**: YAML configuration file handling +- Standard library for HTTP client and JSON processing +- Uses mise.toml for Go version management (latest) + +### Development Notes + +- No test suite currently exists +- Binary excluded from git via .gitignore (redmine, redmine-*) +- Configuration files (.yaml/.yml) excluded from git for security +- API keys are masked in profile display output +- 30-second HTTP timeout for API requests \ No newline at end of file diff --git a/README.md b/README.md index fc0b782..e4a8fa9 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,19 @@ go build -o redmine - `--offset`: オフセット (デフォルト: 0) - `--project`: プロジェクトIDでフィルタ - `--status`: ステータスIDでフィルタ +- `--me`: 現在のユーザーが作成したIssueのみ表示 例: ```bash +# 基本的な使用例 ./redmine issues list --limit 50 --project 1 --status 1 + +# 自分が作成したIssueのみ表示 +./redmine issues list --me + +# 自分が作成したIssueをプロジェクトでフィルタ +./redmine issues list --me --project 1 ``` #### Issue詳細の表示 diff --git a/client/client.go b/client/client.go index 669532c..401e6f0 100644 --- a/client/client.go +++ b/client/client.go @@ -138,6 +138,9 @@ type UpdateIssueData struct { DoneRatio *int `json:"done_ratio,omitempty"` ParentIssueID *int `json:"parent_issue_id,omitempty"` } +type UserResponse struct { + User User `json:"user"` +} func NewClient(baseURL, apiKey string) *Client { return &Client{ @@ -309,10 +312,6 @@ type UsersResponse struct { Users []User `json:"users"` } -type UserResponse struct { - User User `json:"user"` -} - type TrackersResponse struct { Trackers []Tracker `json:"trackers"` } diff --git a/cmd/issues.go b/cmd/issues.go index aa5f96d..d228403 100644 --- a/cmd/issues.go +++ b/cmd/issues.go @@ -30,6 +30,7 @@ func init() { listIssuesCmd.Flags().String("offset", "0", "Offset for pagination") listIssuesCmd.Flags().String("project", "", "Project ID to filter by") listIssuesCmd.Flags().String("status", "", "Status ID to filter by") + listIssuesCmd.Flags().Bool("me", false, "Filter issues authored by current user") // Add flags to show command showIssueCmd.Flags().BoolP("comments", "c", false, "Include comments (journals) in the output") diff --git a/cmd/issues_list.go b/cmd/issues_list.go index 68d92bb..62ce3d9 100644 --- a/cmd/issues_list.go +++ b/cmd/issues_list.go @@ -63,6 +63,18 @@ var listIssuesCmd = &cobra.Command{ params["status_id"] = status } + // Check if --mine flag is set to filter by current user + me, _ := cmd.Flags().GetBool("me") + if me { + // Get current user ID + userResp, err := c.GetCurrentUser() + if err != nil { + fmt.Printf("Error getting current user: %v\n", err) + return + } + params["assigned_to_id"] = fmt.Sprintf("%d", userResp.User.ID) + } + response, err := c.GetIssues(params) if err != nil { fmt.Printf("Error getting issues: %v\n", err) @@ -74,8 +86,35 @@ var listIssuesCmd = &cobra.Command{ return } + // Column widths + const ( + idWidth = 6 + statusWidth = 12 + assigneeWidth = 10 + dateWidth = 12 + ) + fmt.Printf("Issues (Total: %d)\n", response.TotalCount) - fmt.Println(strings.Repeat("-", 100)) + + // Header + fmt.Printf("%-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %s\n", + idWidth, "ID", + statusWidth, "Status", + assigneeWidth, "Assignee", + dateWidth, "StartDate", + dateWidth, "DueDate", + dateWidth, "UpdatedAt", + "Subject") + + // Separator + fmt.Printf("%s-|-%s-|-%s-|-%s-|-%s-|-%s-|-%s\n", + strings.Repeat("-", idWidth), + strings.Repeat("-", statusWidth), + strings.Repeat("-", assigneeWidth), + strings.Repeat("-", dateWidth), + strings.Repeat("-", dateWidth), + strings.Repeat("-", dateWidth), + strings.Repeat("-", 7)) for _, issue := range response.Issues { assignedTo := "Not assigned" @@ -83,13 +122,30 @@ var listIssuesCmd = &cobra.Command{ assignedTo = issue.AssignedTo.Name } - fmt.Printf("#%d | %s | %s | %s | %s | %s\n", - issue.ID, - truncateString(issue.Subject, 40), - issue.Status.Name, - issue.Priority.Name, - assignedTo, - issue.UpdatedOn.Format("2006-01-02")) + startDate := "-" + if issue.StartDate != nil { + startDate = *issue.StartDate + } + + dueDate := "-" + if issue.DueDate != nil { + dueDate = *issue.DueDate + } + + // Truncate long fields to fit column widths + status := issue.Status.Name + if len(status) > statusWidth { + status = status[:statusWidth-3] + "..." + } + + fmt.Printf("#%-*d | %-*s | %-*s | %-*s | %-*s | %-*s | %s\n", + idWidth-1, issue.ID, // -1 for the # prefix + statusWidth, status, + assigneeWidth, assignedTo, + dateWidth, startDate, + dateWidth, dueDate, + dateWidth, issue.UpdatedOn.Format("2006-01-02"), + issue.Subject) } }, }