Noun-first commands, multi-profile support, semantic WP identifiers, and new commands#15
Open
cbliard wants to merge 61 commits into
Open
Noun-first commands, multi-profile support, semantic WP identifiers, and new commands#15cbliard wants to merge 61 commits into
cbliard wants to merge 61 commits into
Conversation
New noun-first commands (old verb-first commands kept during transition): op workpackage list/create/update/inspect (was: op list/create/update/inspect workpackage[s]) op activities list [--wp id] (was: op list activities [id]) op project list/inspect (was: op list/inspect project[s]) op user search (was: op search user) op timeentry list (was: op list timeentries) op type list (was: op list types) op status list (was: op list status) op notification list (was: op list notifications) op activities list uses --wp flag instead of positional argument to allow future scoping (--project, global) without breaking the interface. op git start workpackage is unchanged.
Deleted: cmd/list/, cmd/create/, cmd/update/, cmd/inspect/, cmd/search/ Remaining commands: op workpackage list/create/update/inspect op activities list [--wp id] op project list/inspect op user search op timeentry list op type list op status list op notification list op git start workpackage (unchanged) op login (unchanged)
- --wp renamed to --work-package for consistency with other flag names - MarkFlagRequired ensures Cobra validates the flag before execution rather than failing at runtime
Introduce a Renderer interface with TextRenderer (existing behavior) and JsonRenderer (new) implementations. The active renderer is selected at startup via the root command's PersistentPreRun based on the --format flag value.
The predicate (Id == target) is not monotone — sort.Search requires >=. Also, len(users)-1 as the upper bound excluded the last user from the search. Together, these bugs caused the wrong user name to be resolved when the target user appeared before the last position in the slice.
Cobra supports hyphens in command names; aligning with common CLI conventions (docker, kubectl, gh). Package names remain unhyphenated as Go requires. Also updates README and CLAUDE.md to reflect noun-first syntax throughout.
go build -o op produces a local binary that should not be versioned.
Lists budgets for a given project via GET /api/v3/projects/{id}/budgets.
Fetches a single budget by ID via GET /api/v3/budgets/{id}.
These flags are irrelevant for shell completion generation.
Assignee was already supported for update and display; create was missing it. Takes a user ID, consistent with the update command.
Supports multi-line markdown descriptions. Format is hardcoded to markdown as textile is no longer supported in OpenProject.
Shows configured server URL and current user info. Handles missing config (not logged in) and 401 (invalid token) gracefully.
Supports --work-package (required), --hours (required), --activity (optional, looked up by name), --spent-on (default: today), --user, --comment flags. Also fixes nil pointer dereferences in TimeEntryDto.Convert() for optional linked resources (project, work package, user, activity, comment).
When GET /api/v3/time_entries/activities returns an error (endpoint not available in all OpenProject versions), show a clear message instead of the raw API error. When the endpoint works but the name doesn't match, list available activities.
Previously the CLI stored a single host+token pair in a flat config file,
making it awkward to switch between instances (community, self-hosted, staging,
etc.). Users had to re-run `op login` each time, overwriting their credentials.
This commit introduces named profiles so multiple instances can be configured
and used side-by-side.
Config format
The config file (~/.config/openproject/config, or $XDG_CONFIG_HOME) is now
stored as INI with one section per profile:
[default]
host = https://community.openproject.org
token = abc123
[work]
host = https://work.example.com
token = xyz789
Existing single-line files ("host token") are silently migrated to [default]
on the first read.
Profile names
Only letters, digits, - and _ are allowed, with no leading/trailing hyphens.
The interactive prompt sanitizes invalid input and re-prompts with the
corrected name as the new default.
op login
- Prompts "Profile name? [default]" before asking for credentials.
- --profile <name> skips the prompt and uses that name directly (validated
upfront; errors immediately if the name is invalid).
- OP_CLI_PROFILE env var behaves the same as --profile for login.
- If the profile already exists the user is asked to confirm overwrite.
op logout (new command)
- Removes a profile from the config file.
- Defaults to "default"; use --profile to target another.
- Idempotent: no error if the profile does not exist.
- Always asks for confirmation before deleting.
op whoami
- Without --profile: shows every configured profile (server + user), each
block separated by a blank line.
- With --profile <name>: shows only that profile.
- Output now includes a "Profile:" header line per entry.
All other commands
- Accept a global --profile flag (persistent, default "default").
- OP_CLI_PROFILE env var sets the default profile; --profile overrides it.
- OP_CLI_HOST / OP_CLI_TOKEN still override everything (highest priority).
- If an explicitly named profile does not exist, an error is shown and
nothing is done.
Hiding flags via InheritedFlags().MarkHidden() mutates the shared *Flag structs, accidentally hiding --format and --verbose from all sub-commands. Removing the loop lets them appear correctly under "Global Flags" everywhere.
Allows listing the direct children of a work package by passing its ID: op work-package list --parent-id 42 The flag translates to a parent filter on the OpenProject API v3 (operator "="), which is ANDed with any other active filters such as --project-id, --status, or --type. A pre-flight lookup verifies that the given work package exists before issuing the filter query. If it does not, the command exits with a clear error naming the flag: --parent-id: work package #42 not found. Only direct children are returned (depth 1). Listing a full subtree would require multiple API calls and is out of scope.
All commands that reference a project — op project inspect, op work-package list, op work-package create, and op budget list — now accept both the numeric project ID (e.g. 42) and the human-readable project identifier found in the URL (e.g. "foobar" from /projects/foobar/work_packages). Changes: - --project-id flag on op work-package list renamed to --project; the old name is kept as a hidden alias for backward compatibility - op project inspect updated to accept [id|identifier] as its argument; the previous hard rejection of non-numeric input is removed - op budget list --project flag updated to accept string values - op work-package create --project flag updated to accept string values - Project identifier field added to models, DTOs, and JSON output so it is visible when listing or inspecting projects - Project display format updated to "#14 MyProject (my-project)" so users can discover the identifier to reuse in flags - routes.ProjectUrl now builds the human-readable URL (projects/my-project) instead of the numeric one (projects/14) - Input validated client-side: only alphanumeric characters, -, _, and + are accepted; invalid input is rejected before any API call with a clear error message pointing to the offending flag - A friendly 404 error is shown when the project is not found instead of the raw API error response
OpenProject community now supports Jira-style identifiers introduced in https://community.openproject.org/work_packages/72427. All commands that accept a work package ID (inspect, update, list --parent-id, activities --work-package, git start workpackage) now accept both numeric IDs for backward compatibility and project-based identifiers (PROJECTID-NUMBER). Validation enforces the identifier rules: uppercase letters, digits, or underscores; max 10 characters; must start with a letter; followed by a hyphen and a numeric sequence number.
…ayId field The OpenProject API returns a displayId field alongside the numeric id. When project-based identifiers are enabled, this is the semantic key (e.g. SJF-13); on instances without the feature it matches the numeric id and the display falls back to the conventional #N format. Wire displayId through DTO → model → printer (text and JSON renderers). Column alignment in list output handles mixed numeric/#N and semantic IDs.
There was a problem hiding this comment.
Pull request overview
This PR is a broad refactor and feature expansion of the OpenProject CLI, primarily restructuring commands into a noun-first hierarchy while adding multi-profile configuration, semantic work package identifiers, JSON output, and several new resource commands (time entries, budgets, whoami).
Changes:
- Reworked CLI command structure to
op <noun> <verb>and introduced new command groups (work-package,time-entry,project,budget,status,notification,user, etc.). - Added multi-profile configuration support (INI-based), along with
op whoami,op logout, and profile selection via--profile/OP_CLI_PROFILE. - Added semantic identifier handling for work packages/projects, JSON output rendering via a new renderer abstraction, and new resource capabilities (time entry create, budget list/inspect).
Reviewed changes
Copilot reviewed 95 out of 96 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| TODO.md | Tracks follow-up items discovered during review. |
| README.md | Updates documentation/examples to noun-first commands and multi-profile usage. |
| .gitignore | Adds built binary and Claude local settings file. |
| CLAUDE.md | Adds contributor/developer architecture and conventions documentation. |
| models/work_package.go | Adds DisplayId to support semantic display identifiers. |
| models/project.go | Adds Identifier to support slug/identifier-based project access. |
| models/time_entry_activity.go | Adds time entry activity model. |
| models/budget.go | Adds budget model. |
| dtos/work_package.go | Maps API displayId and hardens link/description conversion against nils. |
| dtos/project.go | Adds project identifier mapping. |
| dtos/time_entry.go | Renames/exports links DTO and hardens conversion against nil links/comment. |
| dtos/time_entry_activity.go | Adds DTOs and conversion for time entry activities collection. |
| dtos/budget.go | Adds budget DTOs and collection conversion. |
| components/paths/paths.go | Updates path helpers to accept string identifiers; adds budgets/time entry activities endpoints. |
| components/routes/routes.go | Changes browser routes to use wp/<displayId> and projects/<identifier>. |
| components/resources/work_packages/validate.go | Adds identifier validation for numeric vs semantic WP IDs. |
| components/resources/work_packages/validate_test.go | Tests new WP identifier validation. |
| components/resources/work_packages/read.go | Converts WP APIs to accept string IDs and adds parent filter handling. |
| components/resources/work_packages/filters.go | Adds Parent filter option and filter builder. |
| components/resources/work_packages/create.go | Allows project identifier string; adds assignee/description create options. |
| components/resources/work_packages/update.go | Allows string WP id; adds description patching and improves assignee parsing errors. |
| components/resources/work_packages/activities.go | Switches WP activities to accept string WP identifiers. |
| components/resources/time_entries/create.go | Implements time entry creation plus activity lookup. |
| components/resources/projects/functions.go | Updates project lookup to accept numeric ID or identifier and validates input. |
| components/resources/projects/versions.go | Updates versions API to take string project identifiers/IDs. |
| components/resources/projects/validate.go | Adds project identifier validation helper. |
| components/resources/projects/validate_test.go | Tests new project identifier validation. |
| components/resources/budgets/functions.go | Adds budgets lookup and listing for a project. |
| components/configuration/util.go | Removes legacy single-profile read/write logic (replaced by profiles). |
| components/configuration/profiles.go | Implements INI-based multi-profile config + migration from legacy format. |
| components/configuration/profiles_test.go | Comprehensive tests for profile sanitization/validation/read/write/migration. |
| components/printer/renderer.go | Introduces renderer abstraction and --format handling. |
| components/printer/renderer_test.go | Tests renderer init behavior on unknown/known formats. |
| components/printer/text_renderer.go | Centralizes existing text rendering logic behind renderer interface. |
| components/printer/json_renderer.go | Adds JSON output rendering for list/inspect commands. |
| components/printer/work_packages.go | Updates WP printing to use DisplayId via renderer and adds formatting helpers. |
| components/printer/work_packages_test.go | Updates tests for DisplayId and adds semantic display ID alignment test. |
| components/printer/projects.go | Routes project output via renderer. |
| components/printer/projects_test.go | Updates expected project output to include identifier. |
| components/printer/users.go | Routes user output via renderer. |
| components/printer/types.go | Routes type output via renderer. |
| components/printer/status.go | Routes status output via renderer. |
| components/printer/time_entries.go | Routes time entry output via renderer. |
| components/printer/notifications.go | Routes notifications output via renderer while keeping grouping helper. |
| components/printer/custom_actions.go | Routes custom actions output via renderer. |
| components/printer/numbers.go | Routes numeric output via renderer. |
| components/printer/budgets.go | Adds budgets printer entry points routed through renderer. |
| components/printer/budgets_test.go | Adds budgets output/alignment tests. |
| components/printer/whoami.go | Adds whoami printer entry point routed through renderer. |
| components/printer/whoami_test.go | Adds whoami output test. |
| components/printer/activities.go | Routes activities output via renderer. |
| components/printer/activities_test.go | Adds regression test for prior activities user-lookup bug. |
| cmd/root.go | Registers new noun-first command groups; adds --format and --profile; initializes renderer/requests in pre-run. |
| cmd/login.go | Adds profile selection/prompting and profile overwrite confirmation; stores credentials per profile. |
| cmd/logout.go | Adds logout command to remove stored profiles with confirmation. |
| cmd/whoami.go | Adds whoami command to display authenticated user for one/all profiles. |
| cmd/workpackage/workpackage.go | Adds new work-package command group and wires flags/subcommands. |
| cmd/workpackage/list.go | Implements work-package list with project/parent filters and improved errors. |
| cmd/workpackage/list_flags.go | Defines flags for work-package list (project, parent-id, filters, total, etc.). |
| cmd/workpackage/create.go | Implements work-package create supporting project identifier, assignee, description, open-in-browser. |
| cmd/workpackage/update.go | Implements work-package update supporting semantic IDs and new --description. |
| cmd/workpackage/options_test.go | Adds tests for description flag semantics in create/update. |
| cmd/workpackage/inspect.go | Implements work-package inspect supporting semantic IDs and listing types. |
| cmd/workpackage/workpackage_test.go | Adds test ensuring hyphenated command name usage. |
| cmd/timeentry/timeentry.go | Adds time-entry command group and wires list/create. |
| cmd/timeentry/list.go | Implements time-entry list as noun-first list subcommand. |
| cmd/timeentry/list_flags.go | Wires filters for time-entry list. |
| cmd/timeentry/create.go | Implements time-entry create including defaults and optional activity/user/comment. |
| cmd/timeentry/create_flags.go | Defines flags and required inputs for time-entry create. |
| cmd/timeentry/timeentry_test.go | Adds test ensuring hyphenated command name usage. |
| cmd/project/project.go | Adds project command group and wires list/inspect with open flag. |
| cmd/project/list.go | Implements project list as noun-first list subcommand. |
| cmd/project/inspect.go | Implements project inspect accepting numeric ID or identifier. |
| cmd/budget/budget.go | Adds budget command group and wires list/inspect. |
| cmd/budget/list.go | Implements `budget list --project <id |
| cmd/budget/inspect.go | Implements budget inspect <id>. |
| cmd/status/status.go | Adds status command group. |
| cmd/status/list.go | Implements status list. |
| cmd/notification/notification.go | Adds notification command group. |
| cmd/notification/list.go | Implements notification list with reason filter. |
| cmd/user/user.go | Adds user command group. |
| cmd/user/search.go | Implements user search (including me keyword behavior). |
| cmd/wptype/wptype.go | Adds type command group for work package types. |
| cmd/wptype/list.go | Implements type list. |
| cmd/activities/activities.go | Adds activities command group. |
| cmd/activities/list.go | Implements `activities list --work-package <id |
| cmd/git/start/work_package.go | Updates git start command to accept semantic WP identifiers. |
| cmd/update/update.go | Removes legacy verb-first update command group. |
| cmd/update/work_package.go | Removes legacy verb-first update workpackage command. |
| cmd/list/list.go | Removes legacy verb-first list command group. |
| cmd/list/activities.go | Removes legacy verb-first activities list command. |
| cmd/list/status.go | Removes legacy verb-first status list command. |
| cmd/inspect/inspect.go | Removes legacy verb-first inspect command group. |
| cmd/create/create.go | Removes legacy verb-first create command group. |
| cmd/create/work_package.go | Removes legacy verb-first create workpackage command. |
| cmd/search/search.go | Removes legacy verb-first search command group. |
Comments suppressed due to low confidence (2)
cmd/workpackage/list.go:156
- validatedVersionId() logs an error but continues, which can lead to a nil dereference (project may be nil, yet project.Identifier is accessed on the next lines). It should abort/return immediately when projects.Lookup fails.
cmd/workpackage/list.go:161 - validatedVersionId() logs an error but continues, which can lead to using an uninitialized versions slice (and inconsistent CLI behavior). Exit/return when AvailableVersions fails.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Handle url.Parse error in PersistentPreRunE instead of silently ignoring it - Route printJson output through activePrinter instead of fmt directly - Use printer.Input for API token prompt in login instead of fmt.Printf - Add comment explaining DisplayId is always non-empty (API guarantees identifier.presence || id)
Drop omitempty from lockVersion so a zero value is still sent in PATCH bodies; omitting it made every update of a fresh work package fail. Propagate update errors and exit nonzero instead of masking.
The helper has no callers since version filtering moved to the filters package. It also contained an unreachable nil dereference after a printed-but-ignored lookup error, flagged in review.
Info, Done, Error, ErrorText, Debug, Input, and the spinner now write to standard error, so --format json output stays parseable. Version text moves to a new printer.Output which stays on stdout.
All leaf commands convert from Run to RunE and return a sentinel ErrHandled after printing errors, so main exits 1 without printing twice. Replaces scattered os.Exit calls, including exit code 255 from os.Exit(-1). Filter validation and --parent-id lookups now report the real error instead of exiting or masking it as 404.
Rewrite the config via a same-directory temp file and rename, so interrupted writes cannot truncate it and migration resets a permissive legacy mode to 0600. Migration is now best effort for read-only configs. Deleting removes duplicate sections and errors on missing profiles; hosts are validated as absolute URLs on read.
With OP_CLI_HOST/OP_CLI_TOKEN set, whoami shows the environment identity instead of querying the same server once per stored profile. Multi-profile output renders through a new WhoamiList so JSON mode emits one array instead of several top-level objects.
Filter values are JSON-marshaled so quotes cannot corrupt query filters. Activity matching rejects empty and ambiguous input, hours reject NaN/Inf and sub-second values, and activity actors are sorted before the binary search in the renderers. Work package output and browser URLs fall back to the numeric id when the server omits displayId. Completion and --version work offline.
Remove the nonexistent --status update flag from the README, document the full profile-name rules and the breaking verb-first to noun-first migration, correct the activity and inspect --types descriptions, and gofmt files the formatter flagged.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 120 out of 124 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
components/resources/work_packages/activities.go:16
- Activities() logs the request error but continues parsing the (likely nil/empty) response, which can lead to misleading parse errors or panics. It should return immediately on request failure and propagate the error to the caller (which already handles it). Also fix the misspelled named return variable (
activites).
Unknown formats previously printed a warning and continued with text output. Return a handled error before command execution so scripts never receive output in an unexpected format.
Abort before POST when type resolution fails. Once creation succeeds, report browser-launch problems as warnings so callers do not retry and create duplicates.
Resolve actions, patch values, and attachment paths before the first mutation. This prevents local validation errors from leaving earlier changes applied while preserving lock-version refreshes. Attachment validation also requires the addAttachment link and rejects fog storages before any mutation, so a missing upload capability surfaces as an error instead of a panic after the action and patch have already run.
Return request failures from activity and user lookups instead of printing and continuing. The command now emits one diagnostic and stops before rendering incomplete activity data.
Read complete environment credentials without touching the config directory. Reject non-HTTP hosts from environment variables or stored profiles before request setup.
Always invoke the collection renderer, even when a search has no matches. JSON callers now receive [] while text users still get an empty-result diagnostic on standard error.
Treat browser launch as part of read-only inspect commands. Return a handled error so scripts do not mistake an unopened page for success.
Complete the noun-first migration table and remove resolved TODOs. Also describe cross-request partial failures for multi-option updates.
validateAttachment and upload dereferenced WorkPackageDto.Links without a nil check, so a server payload lacking _links, or a work package re-fetched after a custom action that dropped the addAttachment link, panicked the CLI. Convert() was already hardened for nil Links; make both attachment paths agree by returning a handled error instead.
readOrMigrate routed anything not starting with '[' to the legacy "host token" path, so a valid INI file whose first line was a comment failed the host check and was rejected as corrupt, locking the user out of every command. Detect INI by parsing, since parseIni already skips comment and blank lines, and fall back to the legacy format only when no sections are found.
whoami --format json returned a bare object for an explicit profile or environment credentials but an array for the all-profiles listing, so scripts had to branch on the invocation. Route every path through WhoamiList so the JSON output is always an array, consistent with the list and search commands.
On a failed create or update --type, the available-types list was rendered through the active renderer to stdout, so --format json emitted a types array instead of nothing. Add printer.AvailableTypes, which lists names on stderr, and use it on both not-found paths so stdout carries only the command result.
ProjectUrl built projects/<identifier> with no fallback, so a project whose identifier is empty produced projects/ (the index) instead of the project. Mirror WorkPackageUrl and use the numeric id, which is also a valid /projects/ address, when the identifier is missing.
activityCreate printed guidance and then returned a plain error that the command layer reported a second time, so failures showed twice. Return ErrHandled from the paths that already printed, and skip re-reporting it in the caller, matching how work_packages signals an already-reported error.
Comment on lines
+78
to
+90
| if host == "" && explicit { | ||
| printer.Error(openerrors.Custom(fmt.Sprintf( | ||
| "Profile %q not found. Run 'op login --profile %s' to create it.", | ||
| profile, profile, | ||
| ))) | ||
| return openerrors.ErrHandled | ||
| } | ||
|
|
||
| parse, err := url.Parse(host) | ||
| if err != nil { | ||
| printer.ErrorText(fmt.Sprintf("invalid host URL %q: %s", host, err)) | ||
| return openerrors.ErrHandled | ||
| } |
Comment on lines
+182
to
+185
| func printProject(p *models.Project) { | ||
| id := fmt.Sprintf("#%d", p.Id) | ||
| activePrinter.Printf("%s %s (%s)\n", Red(id), Cyan(p.Name), p.Identifier) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR brings a series of improvements to the OpenProject CLI, built incrementally
over ~20 commits. The changes span command naming, new features, and identifier handling.
Sorry for not splitting it in multiple PRs, but I don't have much time for that, and each commits build on top of each other.
Command restructuring (breaking change)
Commands now follow
op <noun> <verb>instead ofop <verb> <noun>. The oldverb-first syntax has been removed entirely:
op list workpackagesop work-package listop create workpackageop work-package createop update workpackageop work-package updateop list time-entriesop time-entry listEach noun (
work-package,time-entry,project,budget, …) is now its ownsubcommand group with discoverable verbs and consistent flag help.
Multi-profile support
op loginnow supports named profiles for working with multiple OpenProject instances:The
OP_CLI_PROFILEenv var selects a profile for the whole session.New commands
op whoami— shows all configured profiles and the authenticated user for eachop time-entry create— creates a time entry with activity, hours, date, commentop budget list/op budget inspect— lists and inspects project budgetsop work-package search— searches work packages by subject, type, status, project name, or identifier; multiple words are ANDed; returns up to 100 resultsNew flags
op work-package list --parent-id— filter by parent work packageop work-package create/update --assignee— assign a userop work-package create/update --description— set descriptionSemantic work package identifiers
Work packages can now be referenced by their project-based identifier wherever a
numeric ID was previously required. Both slug-based identifiers (e.g.
PROJ-42,used before semantic identifiers were introduced) and semantic identifiers
(e.g.
PROJ-123) are supported. ThedisplayIdfield from the API drives display:semantic form when project-based identifiers are enabled,
#Notherwise.JSON output
All list/inspect commands support
--format jsonfor machine-readable output.Project identifiers
Project path arguments now accept either a numeric ID or a human-readable slug
(e.g.
my-project), matching what the OpenProject API supports natively.Developer docs
Added
CLAUDE.mddocumenting the architecture, conventions, and data flow forcontributors.
Suggested test plan
go test ./...passesop login --profile <name>creates and stores a named profileop whoamilists all profiles with their URLs and usersop work-package list --format jsonreturns valid JSONop work-package list --parent-id <id>filters correctlyop time-entry createprompts for missing required fields and submitsPROJ-42orPROJ-123in inspect/update/openop budget listandop budget inspectwork on a project with budgetsop work-package search cascadereturns matching work packagesop work-package search fix loginANDs both termsBreaking change
The verb-first commands from 0.5.5 (
op list workpackages,op update workpackage, …) are removed without aliases; the README now contains a migration table to the noun-first forms.Deferred review items
Findings from external review that are intentionally not addressed in this PR: (1)
whoamire-initializes the global HTTP client once per profile — works correctly, to be revisited when the client stops being a global singleton; (2)components/resourcessometimes prints user hints directly viaprinter— consistent with existing layering, to be cleaned up in a dedicated pass; (3)--activityresolution usesGET /api/v3/time_entries/activities, which 404s on some instances — switching to the form/schema endpoint is tracked in TODO.md; (4) config writes are atomic (temp file + rename) but not file-locked against concurrent logins — deemed unnecessary for an interactive CLI.🤖 Follow-up fixes (post-review)
Added by an agent on Alex's behalf. A code-review pass over the branch surfaced some smaller correctness and output-contract issues, fixed in commits on top of the original work above. These do not change any of the features or behaviours described in the sections above.
_links(and against anaddAttachmentlink dropped by a custom action) soop work-package update --attachreturns a handled error instead of panicking.host tokenformat and rejected as corrupt, locking the user out of every command.op whoami --format jsonnow always emits an array, including for a single or explicit profile, so scripts no longer branch on the invocation. Only the JSON shape changed; the per-profile global-client re-initialization noted in deferred item (1) above is still deferred.--type, the "available types" hint now goes to stderr, keeping--format jsonstdout clean on failure — a partial step toward deferred item (2); resource-layer hints are otherwise unchanged.ProjectUrlfalls back to the numeric id when a project's identifier is empty, instead of linking to the projects index.op time-entry createreports an unresolved--activityonce instead of twice. The endpoint itself is unchanged, so deferred item (3) still applies.