Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
04c6bc6
Add CLAUDE.md with developer conventions and architecture guide
cbliard Mar 31, 2026
d8babc5
Add noun-first commands alongside existing verb-first commands
cbliard Mar 31, 2026
be6b36e
Remove verb-first commands, complete noun-first migration
cbliard Mar 31, 2026
a43a50c
Fix activities list: rename --wp to --work-package and mark required
cbliard Mar 31, 2026
365ff65
Add JSON output support via --format flag
cbliard Apr 1, 2026
91be87b
Print error for unknown --format value instead of silently falling ba…
cbliard Apr 1, 2026
6fc5b0d
Fix sort.Search predicate in Activities user lookup
cbliard Apr 1, 2026
362f2a1
Use noun-first hyphenated command names (work-package, time-entry)
cbliard Apr 1, 2026
0a08f0a
Ignore op binary in git
cbliard Apr 1, 2026
160c083
Add op budget list command
cbliard Apr 1, 2026
908202e
Add op budget inspect command
cbliard Apr 1, 2026
335b21b
Hide --format and --verbose flags from completion command
cbliard Apr 1, 2026
d0d3915
Add --assignee flag to work-package create
cbliard Apr 3, 2026
8197ca1
Add --description flag to work-package create and update
cbliard Apr 3, 2026
d785b77
Add whoami command
cbliard Apr 3, 2026
3a61c8b
Add op time-entry create command
cbliard Apr 3, 2026
2764dea
Improve error message when activity lookup fails in time-entry create
cbliard Apr 4, 2026
8a22b4b
Add multi-profile support for managing multiple OpenProject instances
cbliard Apr 21, 2026
f435c27
Show global flags in sub-command help
cbliard Apr 24, 2026
ed53fa1
Add --parent-id flag to op work-package list
cbliard Apr 28, 2026
5d97eaa
Accept project identifier (slug) in addition to numeric ID
cbliard Apr 29, 2026
b3c23e7
Accept project-based semantic work package identifiers (e.g. PROJ-123)
cbliard Jun 2, 2026
1624174
Display semantic work package identifier (e.g. SJF-13) from API displ…
cbliard Jun 2, 2026
9dd1da6
Fix issues raised in Copilot review
cbliard Jun 4, 2026
7abdcfa
Fix four bugs raised in code review
cbliard Jun 5, 2026
422a3c7
Add op work-package search command
cbliard Jun 5, 2026
c78815f
Add --project flag to op work-package search
cbliard Jun 5, 2026
747da31
Fix: + is not allowed in project identifers
cbliard Jun 8, 2026
a069d46
Add .claude/op.md reference file for AI agent integration
cbliard Jun 8, 2026
7492763
Report invalid config instead of empty credentials
myabc Jun 14, 2026
552f731
Preserve unknown keys when rewriting config
myabc Jun 14, 2026
0f60793
Keep sub-minute precision in time entry durations
myabc Jun 14, 2026
3caf3a9
Rename activity command from plural activities
myabc Jun 14, 2026
223e1b4
Write config with 0600 to protect API tokens
myabc Jun 14, 2026
19468f2
Emit valid JSON when serialization fails
myabc Jun 14, 2026
03902c0
Drop completed TODO about fmt output
myabc Jun 14, 2026
bb4d918
Reject profile section missing host or token
myabc Jun 14, 2026
5418b34
Warn when config file is world-readable
myabc Jun 15, 2026
ed739d6
Add section about identifiers
cbliard Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions .claude/op.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# OpenProject CLI (`op`)

`op` is a CLI for interacting with OpenProject project management instances. Use it proactively whenever the user discusses work packages, projects, time entries, notifications, or anything related to OpenProject — just as naturally as you'd use `git` for version control or `ls` to explore the filesystem.

## Global flags

These flags apply to every command:

```
--format string Output format: text (default) or json
--profile string Profile name (overrides OP_CLI_PROFILE env var, default "default")
--verbose Print verbose output
```

## Orientation

```bash
op whoami # Show current user and server (verify connection)
op whoami --profile <name> # Same for a specific profile
op project list # Discover available projects (get IDs for other commands)
```

## Identifiers

OpenProject has different identifiers for project and work packages. Mixing them up causes silent wrong-WP bugs.

Since 17.5, semantic identifiers can be enabled. Once enabled, work packages have project-based identifiers like Jira.

### Project id

The internal integer primary key of the database. Examples: `141` or `239`. In JSON output, the **`id`** field on a *project* object.

### Project identifier

The human-readable short string identifying the project uniquely. Unique across all projects of the instance. When semantic identifiers are enabled on the instance, they are alphanumeric characters (e.g., `OP`, `PROJ`, `JIM`). When not, they are slugs (e.g., `openproject`, `stream-jira-exit`, `jira-migrator`). In JSON output, the **`identifier`** field on a *project* object.

### Work package id

The internal integer primary key (e.g., `71305`). Unique across all projects on the instance. Always present regardless of instance configuration. This is the number in `/wp/71305` URLs. In JSON output: the **`id`** field of a *work-package* object.

```bash
op work-package inspect 71305
op work-package inspect --format json 71305 | jq .id # → 71305
```

### Work package semantic identifier (aka display ID, aka project-based identifier)

When enabled by an administrator, each work package gets a project-scoped human label of the form `{PREFIX}-{N}` (e.g., `SJF-6`, `AGILE-54`, `OP-18917`, `JIM-43`). The prefix is the project identifier. Designed to support Jira migrations (existing Jira issue keys can be preserved). Historical numerical IDs remain valid and continue resolving to the same work packages.

When enabled, the CLI and list output show these instead of bare numbers. In JSON output: the **`display_id`** field of a *work-package* object (same as `id` on instances where the feature is off).

```bash
op work-package inspect --format json 71305 | jq .display_id # → "AGILE-54" (if enabled)
# → "71305" (if not enabled)
```

> **Gotcha — never strip the display ID prefix:** when project-based identifiers are enabled, list output shows `OP-7756` or `AGILE-32`. Pass these as-is to `inspect` — stripping the prefix gives a bare integer that resolves to a completely different WP:
> ```bash
> op work-package inspect --format json OP-7756 # correct
> op work-package inspect --format json 7756 # WRONG — different unrelated WP
> ```

### Choosing between id and identifier

Prefer the numeric id in scripts, prefer the semantic identifier in human-facing output like changelogs.

---

## Work packages

IDs accept either an ID (`42`) or an identifier (`PROJ-123`, when enabled) everywhere.

```bash
op work-package list # All visible work packages
op work-package list -p <project-id-or-slug> # Filter by project (numeric ID or identifier/slug)
op work-package list -s open # Filter: open / closed / <id> / comma-separated IDs
op work-package list -s '!<id>' # Exclude a status (prefix with !)
op work-package list -a me # Filter by assignee (me or user ID)
op work-package list -t <type-id> # Filter by type (comma-separated IDs, ! prefix to exclude)
op work-package list -v <version-id> # Filter by version
op work-package list --not-version <id> # Exclude a version
op work-package list --parent-id <wp-id> # Direct children of a work package
op work-package list --include-sub-projects # Include sub-project work packages
op work-package list --sub-project <id> # Limit sub-projects to include (with --include-sub-projects)
op work-package list --not-sub-project <id> # Exclude sub-projects (with --include-sub-projects)
op work-package list --timestamp 2025-01-01 # Work packages as of a date (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ)
op work-package list --total # Show only the total count

op work-package search <query>... # Search by subject, type, status, project name, or identifier
op work-package search <query>... -p <project> # Limit search to a project; multiple words are ANDed; up to 100 results

op work-package inspect <id> # Full details of one work package
op work-package inspect <id> --types # Also list available types on the work package
op work-package inspect <id> --open # Open in default browser

op work-package create "Subject" -p <id> # Create (project accepts numeric ID or slug)
op work-package create "Subject" -p <id> \
--type <type> --assignee <uid> \
--description "markdown text" --open # Full create with all options

op work-package update <id> --subject "…" # Update subject
op work-package update <id> --type <type> # Change type
op work-package update <id> --assignee <uid> # Change assignee
op work-package update <id> --description "…" # Update description (markdown)
op work-package update <id> --action <action> # Execute a custom action (e.g. status transition)
op work-package update <id> --attach <filepath> # Attach a file
```

## Time entries

```bash
op time-entry list # Time entries for current user
op time-entry list -u <user-id> # Time entries for a specific user

op time-entry create \
-w <wp-id> \
--hours 1.5 \
--activity "Development" \
--spent-on 2025-01-15 \
--comment "Fixed the bug" \
-u <user-id> # All flags; --spent-on defaults to today
```

## Budgets

```bash
op budget list -p <project-id-or-slug> # Budgets for a project
op budget inspect <id> # Full details of a budget
```

## Projects

```bash
op project list # All visible projects
op project inspect <id-or-slug> # Project details (numeric ID or identifier/slug)
op project inspect <id-or-slug> --open # Open in default browser
```

## Other resources

```bash
op notification list # Unread notifications
op notification list -r <reason> # Filter by reason

op user search <query> # Find a user

op status list # List work package statuses
op type list # List work package types

op activity list # All activities
op activity list --work-package <wp-id> # Activities for a specific work package
```

## Git integration

```bash
op git start workpackage <id> # Create a branch from WP id/subject/type and switch to it
```

## Profiles (multiple OpenProject instances)

The `--profile` flag selects which instance to talk to (stored in `~/.config/openproject/config`). The default profile is used when omitted. The env var `OP_CLI_PROFILE` also selects a profile.

```bash
op login # Authenticate (prompts for host, token, profile name)
op login --profile work # Authenticate a named profile
op logout # Remove the default profile
op logout --profile work # Remove a named profile
op whoami # Show all profiles with their server and user
```

## JSON output

Use `--format json` to get machine-readable output or raw markdown descriptions:

```bash
op work-package inspect <id> --format json
op work-package list --format json -p <project>
```

## Workflow for use-case analysis

1. `op whoami` — confirm which instance you're on
2. `op project list` — find relevant project IDs
3. `op work-package list -p <id> -s open` — get the work items
4. `op work-package inspect --format json <id>` — drill into specifics (raw markdown)
5. `op work-package search <terms> -p <id>` — find by subject or identifier fragment
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
.fleet
.idea

# Compile output, if running `go build` on a linux/x64
# Compile output
openproject-cli
op

# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
Expand All @@ -26,3 +27,6 @@ openproject-cli

# Go workspace file
go.work

# Claude local settings file
.claude/settings.local.json
113 changes: 113 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# OpenProject CLI — Developer Guide

## Build & Run

```bash
# Build
go build -o op .

# Run tests
go test ./...

# Run a specific test package
go test ./components/printer/...

# Install locally
go install .
```

## Architecture

The codebase is organized in strict layers. Each layer only depends on layers below it.

```
cmd/ # Cobra commands — CLI entry points only, no business logic
components/
resources/ # Business logic — API calls, option handling
paths/ # API path constants (paths.go — single file)
requests/ # HTTP client (GET, POST, PATCH)
parser/ # JSON response parsing
printer/ # Terminal output formatting
routes/ # Browser URL generation
common/ # Shared utilities (string, slice, math)
configuration/ # Multi-profile config (INI), CLI version
launch/ # Browser launcher
models/ # Domain models (plain structs, no logic)
dtos/ # JSON DTOs with Convert() to models
```

### Data flow

API response → `parser.Parse[SomethingDto]()` → `dto.Convert()` → `models.Something` → `printer.Something()`

### DTO conventions

- DTOs live in `dtos/`, named `<Resource>Dto`
- DTOs mirror the OpenProject API v3 HAL JSON structure
- Links use `*LinkDto` with `Href` and `Title` fields, serialized as `_links`
- Every DTO implements `Convert() *models.Something` to produce a domain model
- Collections follow the pattern: `<Resource>CollectionDto` with `Embedded.<Resource>Elements`
- `omitempty` on all JSON fields in DTOs used for POST/PATCH bodies
- `WorkPackageDto` includes a `displayId` field (camelCase, from the API) mapped to `DisplayId` on the model; always present — holds the semantic identifier (e.g. `PROJ-123`) when project-based identifiers are enabled, or the numeric id as a string otherwise

### Command conventions

- Commands follow `op <noun> <verb>` (noun-first), e.g. `op work-package list`, `op time-entry create`
- Each noun has its own package under `cmd/` (`workpackage`, `timeentry`, `project`, `user`, …) — package names use no hyphens even when the command name does (e.g. `work-package` → `package workpackage`)
- Each noun package exposes a `RootCmd` registered in `cmd/root.go`
- One file per verb within each noun package (e.g. `cmd/workpackage/list.go`, `cmd/workpackage/create.go`)
- Flags: always define long flag names; add short flags (`-p`, `-o`) for frequently used ones
- Flags that resolve a resource (e.g. `--type`, `--assignee`) perform an API lookup and store the resolved link in the DTO

### Resource conventions

- Each resource has its own package under `components/resources/`
- Operation types use `iota` enums: `CreateOption`, `UpdateOption`, `FilterOption`
- Operations are dispatched via a `map[Option]func(...)` — add new options by extending the map
- Public API: `Create(...)`, `Lookup(id)`, `All(filters, query, ...)`, `Update(id, options)`

### Configuration conventions

- Config stored as INI at `~/.config/openproject/config` (or `$XDG_CONFIG_HOME/openproject/config`)
- Each profile is an INI section: `[name]` with `host` and `token` keys
- Profile names: letters, digits, `-`, `_` only; no leading/trailing hyphens; validated by `ValidateProfileName`, sanitized by `SanitizeProfileName`
- Key constants: `DefaultProfile = "default"`, `EnvProfile = "OP_CLI_PROFILE"`
- Key functions: `ReadConfig(profile)`, `WriteConfigForProfile(profile, host, token)`, `DeleteProfile(profile)`, `AllProfiles()`
- `OP_CLI_HOST` / `OP_CLI_TOKEN` env vars override all profiles; `OP_CLI_PROFILE` selects a profile (overridden by `--profile` flag)
- Old single-line format (`host token`) is auto-migrated to `[default]` on first read

### Paths conventions

- All API paths are defined in `components/paths/paths.go`
- Functions are named after the resource: `WorkPackage(id)`, `WorkPackages()`, `ProjectWorkPackages(projectId)`
- All paths are relative (no host), starting with `/api/v3`
- Project path functions (`Project`, `ProjectWorkPackages`, `ProjectVersions`, `ProjectBudgets`) take a `string` that may be either a numeric ID (`"42"`) or a human-readable identifier (`"my-project"`); the OpenProject API accepts both forms at the same endpoints
- `WorkPackage(id)` and `WorkPackageActivities(id)` take a `string` that may be either a numeric ID (`"12345"`) or a project-based semantic identifier (`"PROJ-123"`); validated by `work_packages.ValidateIdentifier`

### Printer conventions

- All terminal output goes through `printer/` — never `fmt.Println` directly in commands
- Color scheme: Red = ID, Green = type, Cyan = subject/name, Yellow = status
- `printer.Error(err)` for errors, `printer.ErrorText(msg)` for plain error strings
- `printer.Info(msg)` for progress messages, `printer.Done()` after successful mutations
- Work packages display `DisplayId` from the API: semantic form (e.g. `PROJ-123`) when project-based identifiers are enabled, `#N` for numeric-only systems (where `displayId` equals the numeric id)
- Work package browser URLs use the short `wp/<displayId>` form (e.g. `wp/PROJ-123` or `wp/42`)

## Testing conventions

- Test files use external test packages: `package printer_test`, `package requests_test`
- `TestMain` in `printer_test` initializes shared state (routes, printer) for the package
- Tests use plain `t.Errorf` — no test framework, no assertions library
- Tests only exist for `printer`, `requests`, `common`, and `configuration` — no tests on `cmd/` or `resources/`
- When adding a new printer function, add a corresponding test in `components/printer/`
- When adding a new configuration function, add a corresponding test in `components/configuration/`

## Dependencies

| Package | Purpose |
|---------|---------|
| `github.com/spf13/cobra` | CLI framework |
| `github.com/fatih/color` | Terminal colors (via printer) |
| `github.com/briandowns/spinner` | Progress spinner |
| `github.com/go-git/go-git/v5` | Git integration (`op git` commands) |
| `github.com/sosodev/duration` | ISO 8601 duration parsing |
Loading
Loading