Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 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
d51885f
Fix 409 on update of fresh work packages
myabc Jul 21, 2026
6c50754
Remove dead validatedVersionId helper
myabc Jul 21, 2026
3539483
Route diagnostics to stderr, keep data on stdout
myabc Jul 21, 2026
e5d1e40
Use RunE so failures exit nonzero
myabc Jul 21, 2026
da41274
Harden config writes: atomic, 0600, dup-safe
myabc Jul 21, 2026
44a3b33
Fix whoami for env creds and JSON output
myabc Jul 21, 2026
2bb27de
Fix lookup, validation, and fallback bugs
myabc Jul 21, 2026
e096dd0
Fix docs drift and gofmt formatting
myabc Jul 21, 2026
cf20e5a
Reject unknown output formats
myabc Jul 21, 2026
b570dc9
Report work package creation accurately
myabc Jul 21, 2026
9b3b4d0
Validate work package updates up front
myabc Jul 21, 2026
ac6665a
Propagate activity lookup failures
myabc Jul 21, 2026
e701114
Validate configured hosts before use
myabc Jul 21, 2026
3ba8282
Render empty searches as JSON arrays
myabc Jul 21, 2026
bf4b5c2
Fail when inspect cannot open a browser
myabc Jul 21, 2026
60a279f
Bring CLI documentation up to date
myabc Jul 21, 2026
583b6ea
Guard attachment paths against nil work-package links
myabc Jul 21, 2026
515d1d7
Accept INI config files that open with a comment
myabc Jul 21, 2026
5ae8569
Emit whoami JSON as an array in every case
myabc Jul 21, 2026
9becbff
Send unresolved-type hint to stderr, not stdout
myabc Jul 21, 2026
97fc19f
Fall back to numeric id in project URLs
myabc Jul 21, 2026
eb8b3f6
Report activity lookup failures only once
myabc Jul 21, 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
186 changes: 186 additions & 0 deletions .claude/op.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# 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 # List available types instead of showing 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 --work-package <wp-id> # Activities for a work package (flag is required)
```

## 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
114 changes: 114 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# 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

- Component tests use external test packages (`package printer_test`, `package requests_test`, `package work_packages_test`); cmd-layer tests use internal packages (`package workpackage`) because they exercise unexported command handlers and flag variables
- `TestMain` in `printer_test` initializes shared state (routes, printer) for the package
- Tests use plain `t.Errorf` — no test framework, no assertions library
- Tests exist for `printer`, `requests`, `common`, `configuration`, `components/resources/work_packages`, and the `cmd/` packages (root, `activity`, `project`, `user`, `workpackage`)
- Regression tests for command/resource behaviour use `httptest` localhost servers and count mutating requests to assert no-mutation guarantees
- 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