opctl
v0.1.11
Published
Conservative local CLI bridge for OpenProject API v3
Readme
opctl
opctl is a small local Node.js + TypeScript CLI bridge for OpenProject API v3. It uses the current user's personal API token and is read-only by default.
Install
Published package:
- npmjs.com:
opctl - current package:
[email protected] - binary:
opctl
Install globally:
npm install -g opctl
opctl --helpOr run without a global install:
npx opctl --helpConfiguration
Export variables in your shell, load a local file with --env <path>, or save non-write defaults in a profile. By default, opctl also reads .env from the current working directory when present; pass --no-env to disable that.
Required:
OPENPROJECT_URL: OpenProject instance URL, optionally including an instance path prefix. Server-returned API hrefs are resolved against this URL; when an instance path prefix is repeated in a server href it is stripped, so a prefixed instance URL behaves the same as a bare one.OPENPROJECT_TOKEN: personal OpenProject API token.
Optional:
OPENPROJECT_AUTH_MODE:bearer(default) orbasic. Basic auth uses usernameapikeyand the token as password.OPENPROJECT_DEFAULT_PROJECT: project identifier/id used bywp searchwhen--projectis omitted.OPENPROJECT_ALLOW_WRITE: must be exactly1to allow write-capable commands.
Profile commands:
opctl profile set navlin-qa --url https://openproject.example.com --auth-mode bearer --default-project qa --token ...
opctl profile use navlin-qa
opctl --profile navlin-qa me --json
opctl profile show navlin-qa
opctl profile listProfiles are stored under ${XDG_CONFIG_HOME:-~/.config}/opctl/profiles.json. The file is written with restrictive permissions where supported, tokens may be stored there, and profile display commands redact tokens. OPENPROJECT_ALLOW_WRITE is never loaded from .env files or profiles; writes still require the real process environment variable.
Usage
Show the authenticated OpenProject user:
opctl me
opctl me --jsonInspect API root links:
opctl api-root
opctl api-root --jsonList projects:
opctl projects --page-size 20
opctl projects --jsonRead work packages:
opctl wp get 123
opctl wp get 123 --json
opctl wp get 123 --raw-json
opctl wp get 123 124 --table
opctl wp get --ids 123,124 --fields id,subject,status,assignee --table
opctl wp get --ids 123,124 --jsonl
opctl wp get --ids 123,124 --raw-json--raw-json emits a single raw OpenProject object for one ID and a JSON array for multiple IDs. wp view and wp show are aliases of wp get.
Field selection supports id,subject,status,type,assignee,responsible,project,href,browserUrl,updatedAt,description,shortDescription,attachmentsCount,lockVersion,priority; aliases: title=subject, url=href.
Search work packages:
opctl wp search --project my-project --subject "pump"
opctl wp search --project my-project --assignee-me --status open
opctl wp search --assignee [email protected] --open
opctl wp search --responsible-me --open
opctl wp search --responsible 18 --not-status "In Client Review"
opctl wp search --filter responsible=me --filter status=o --sort updated_at:desc
opctl wp search --open --subject "pump" --compact
opctl wp search --subject "pump" --fields id,subject,status --jsonIf --project is omitted, opctl wp search uses OPENPROJECT_DEFAULT_PROJECT when set. Without either, it searches the instance-wide work package endpoint.
Look up an exact user email, work package types, statuses, and priorities:
opctl users --email [email protected]
opctl users --email [email protected] --json
opctl types --project my-project
opctl types --json
opctl statuses
opctl prioritiesUser references accepted by work package search, create, and update are me, a numeric user id, a /api/v3/users/<id> href, or an exact email address. Exact-email resolution fails clearly when no unique visible user matches.
List work packages assigned to the authenticated user:
opctl wp mine
opctl wp mine --open --table
opctl wp mine --project my-project --page-size 50 --fields id,subject,status,updatedAtList open work packages accountable/responsible to the authenticated user:
opctl wp accountable
opctl wp accountable --table --fields id,subject,status,responsible,updatedAtTriage a known list:
opctl wp check 123 124
opctl wp check --ids 123,124 --fields id,title,status,assignee,shortDescription,attachmentsCount --tableInspect and download work package attachments:
opctl wp attachments 123
opctl wp attachments 123 --json
opctl wp download-attachments 123 --dir /tmp/op-attachments
opctl wp download-attachments 123 --output-dir /tmp/op-attachments
opctl wp download-attachments 123 --dir /tmp/op-attachments --overwrite --jsonUpload files directly to a work package:
OPENPROJECT_ALLOW_WRITE=1 opctl wp upload-attachments 123 report.pdf screenshot.png --dry-run --json
OPENPROJECT_ALLOW_WRITE=1 opctl wp upload-attachments 123 report.pdf screenshot.png --description "Agent evidence" --jsonlwp upload-attachments reads and validates every local file before the first write, rejects duplicate paths, then sends one multipart request per file to /api/v3/work_packages/<id>/attachments. Use --json for one document or --jsonl for one result per file. A network failure after a write is reported as indeterminate, because the server may have accepted the upload.
Show redacted diagnostics:
opctl doctor
opctl doctor --jsonHTTP errors include the request method and safe API path, with focused messages for authentication, visibility, stale lockVersion, and validation failures. Network errors include the attempt count and timeout or underlying cause without exposing query strings or tokens. Only GET requests are retried (network failures and HTTP 429/502/503/504); write requests are never retried automatically.
Pull the OpenAPI spec (defaults to the public community instance):
opctl spec pull
opctl spec pull --output openapi/my-spec.json
opctl spec pull --url https://openproject.example.comWrite-capable commands:
# Comment on a work package
OPENPROJECT_ALLOW_WRITE=1 opctl wp comment 123 --dry-run "Investigating"
OPENPROJECT_ALLOW_WRITE=1 opctl wp comment 123 "Investigating"
# Create a work package (dry-run validates through /api/v3/work_packages/form)
OPENPROJECT_ALLOW_WRITE=1 opctl wp create --project alspc --type Feature --subject "Improve Ask NAVLIN Explore messaging experience" --description-file ticket.md --parent 120 --assignee me --responsible [email protected] --dry-run
OPENPROJECT_ALLOW_WRITE=1 opctl wp create --project alspc --type Feature --subject "S" --json < ticket.md
# Update common fields; clear relationship/text fields explicitly
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 123 --subject "Revised subject" --status "In progress" --priority High --assignee [email protected] --dry-run --json
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 123 --clear-description --clear-parent --clear-assignee --clear-responsible
# Replace one Markdown section of the description, leaving the rest intact
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 24067 --replace-description-section "Backend integration" --description-file api.md
# Fail unless the description already contains a section (assertion)
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 24067 --require-description-section "Uploaded screenshots" --status "In Client Review"
# Re-read after writing and confirm the change landed
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 123 --subject "Revised subject" --verify
# Use a user-story template
opctl wp create --template user-story > ticket.md
OPENPROJECT_ALLOW_WRITE=1 opctl wp create --project alspc --type Feature --subject "New feature" --template user-story --dry-runwp comment, wp create, wp update, attachment uploads, and bulk mutations require OPENPROJECT_ALLOW_WRITE=1. Write-capable commands support --dry-run and avoid mutation in dry-run mode. Dry-run output includes the resolved request url, built by the same code path execution uses, so the preview always matches the actual request. Nested request details are JSON-formatted in text output instead of being collapsed to [object Object].
wp create supports parent, assignee, and responsible links in addition to the common type/status/priority fields. It validates through /api/v3/work_packages/form before creating. Description can come from --description <text>, --description-file <path> (use - for stdin), piped stdin, or --template user-story.
wp update supports subject, description, type, status, priority, parent, assignee, and responsible. Use --clear-description, --clear-parent, --clear-assignee, or --clear-responsible instead of relying on ambiguous empty values. It reads the current work package, validates through its form endpoint, and includes the current lockVersion in the PATCH; --dry-run emits that validated request without sending it. --replace-description-section <heading> rewrites only the body of the named Markdown heading (matched case-insensitively) and preserves every other section; the new body comes from --description or --description-file. --require-description-section <heading> aborts with a validation error when the current description lacks that heading. --verify re-reads the work package after a successful write and fails if the returned subject/description do not match what was sent.
Type, status, and priority references accept a case-insensitive exact name, numeric id, or full /api/v3/... href. Parent references accept an id or href. User references accept me, id, href, or exact email.
Bulk work package mutations
Bulk create and update consume JSONL from a file or stdin and emit JSONL by default:
OPENPROJECT_ALLOW_WRITE=1 opctl wp bulk create --file creates.jsonl --dry-run
OPENPROJECT_ALLOW_WRITE=1 opctl wp bulk update --file -Each create record requires project, type, and subject; it may also contain description, status, priority, parent, assignee, responsible, and an optional unique caller-defined key that is echoed in the result:
{"key":"feature-1","project":"alspc","type":"Feature","subject":"Agent-created feature","assignee":"me","responsible":"[email protected]"}Each update record requires a positive id plus at least one update field. Set description, parent, assignee, or responsible to null to clear it:
{"key":"feature-1","id":123,"status":"In progress","assignee":"[email protected]"}
{"id":124,"description":null,"parent":null,"responsible":null}The entire manifest is parsed and server-preflighted before execution. Any local or server preflight error prevents all writes. After a successful preflight, mutations run sequentially and continue after individual failures; every line receives a result, and network failures during writes are marked indeterminate because their server-side outcome is unknown. --dry-run reports one concise record per line (method, path, and resolved url only, no payload) plus a N validated, 0 conflicts, 0 written summary on stderr, so large manifests do not flood the terminal.
OpenAPI
The repository commits openapi/openproject.json and generated types in src/generated/openproject.ts. The committed spec is an auditable public OpenProject baseline.
npm run openapi:pull and opctl spec pull default to the official public spec at https://community.openproject.org. They do not read OPENPROJECT_URL or OPENPROJECT_TOKEN, so running tests or pulling the spec never sends credentials to a private instance.
# Refresh from the public community spec (safe, no credentials needed)
npm run openapi:update
# Pull from a specific private instance (explicit opt-in)
OPENPROJECT_SPEC_URL=https://openproject.example.com \
OPENPROJECT_SPEC_TOKEN=... \
npm run openapi:pull
# Or via the CLI
opctl spec pull --url https://openproject.example.comPrivate-instance pulls use dedicated OPENPROJECT_SPEC_URL / OPENPROJECT_SPEC_TOKEN / OPENPROJECT_SPEC_AUTH_MODE variables. Normal OPENPROJECT_URL and OPENPROJECT_TOKEN are never used for spec pulling.
Build and verification
npm run typecheck
npm run test
npm run build
node dist/cli.js --help
node dist/cli.js wp --helpSafety model
- No token or
Authorizationheader is printed by normal errors, JSON output, spec pulling, or tests. - Spec pulling defaults to the public community spec and ignores
OPENPROJECT_URL/OPENPROJECT_TOKEN; private-instance pulls require explicit--urlorOPENPROJECT_SPEC_URL. - Local
.envfiles are loaded for read configuration by default;--no-envdisables that, and.envcannot enable writes. - OpenProject writes are blocked unless the real process environment contains
OPENPROJECT_ALLOW_WRITE=1exactly. - Every write-capable command supports
--dry-runand avoids mutation in dry-run mode. wp updateand bulk update read the currentlockVersionand include it in the PATCH, so a concurrent edit between read and write is rejected by OpenProject rather than silently overwritten (optimistic locking).- No delete, close, archive, or move commands are implemented. Bulk create/update is available, but only after whole-manifest preflight and with per-record results.
