Skip to content
76 changes: 76 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 <id>` - 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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,19 @@ go build -o redmine
- `--offset`: オフセット (デフォルト: 0)
- `--project`: プロジェクトIDでフィルタ
- `--status`: ステータスIDでフィルタ
- `--me`: 現在のユーザーが作成したIssueのみ表示

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description for the --me flag is incorrect. Based on the code implementation, this flag filters issues assigned to the current user, not issues created by the current user. The description should be updated to reflect this.

Copilot uses AI. Check for mistakes.

例:

```bash
# 基本的な使用例
./redmine issues list --limit 50 --project 1 --status 1

# 自分が作成したIssueのみ表示
./redmine issues list --me

# 自分が作成したIssueをプロジェクトでフィルタ
./redmine issues list --me --project 1
Comment on lines +80 to +84

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments in the examples incorrectly describe the --me flag as filtering issues created by the user, when it actually filters issues assigned to the user. These comments should be corrected.

Copilot uses AI. Check for mistakes.
```

#### Issue詳細の表示
Expand Down
7 changes: 3 additions & 4 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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"`
}
Expand Down
1 change: 1 addition & 0 deletions cmd/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag description is incorrect. Based on the implementation in issues_list.go, this flag filters issues assigned to the current user (using assigned_to_id), not issues authored by the current user.

Suggested change
listIssuesCmd.Flags().Bool("me", false, "Filter issues authored by current user")
listIssuesCmd.Flags().Bool("me", false, "Filter issues assigned to current user")

Copilot uses AI. Check for mistakes.

// Add flags to show command
showIssueCmd.Flags().BoolP("comments", "c", false, "Include comments (journals) in the output")
Expand Down
72 changes: 64 additions & 8 deletions cmd/issues_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ var listIssuesCmd = &cobra.Command{
params["status_id"] = status
}

// Check if --mine flag is set to filter by current user

Copilot AI Sep 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment refers to a --mine flag, but the actual flag name is --me. This comment should be updated for consistency.

Suggested change
// Check if --mine flag is set to filter by current user
// Check if --me flag is set to filter by current user

Copilot uses AI. Check for mistakes.
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)
Expand All @@ -74,22 +86,66 @@ 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"
if issue.AssignedTo != nil {
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)
}
},
}
Loading