Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
21d58fb
docs(specs): add multi-profile auth design spec
Zious11 Apr 25, 2026
32e8f77
docs(specs): add Windows reserved-name validation + concurrency notes
Zious11 Apr 25, 2026
36e3c5b
docs(plan): add multi-profile auth implementation plan
Zious11 Apr 25, 2026
f9bf9c4
feat(config): validate profile names (regex + Windows reserved)
Zious11 Apr 25, 2026
e122655
feat(config): add ProfileConfig type alongside legacy InstanceConfig
Zious11 Apr 25, 2026
cf5d939
feat(config): resolve active profile name from precedence chain
Zious11 Apr 25, 2026
9fe4f21
fix(config): clarify error wording + JR_PROFILE_OVERRIDE doc
Zious11 Apr 25, 2026
89e73ae
feat(config): auto-migrate legacy [instance] block into [profiles.def…
Zious11 Apr 25, 2026
ed4d166
refactor(auth): namespace OAuth tokens by profile + lazy migrate lega…
Zious11 Apr 25, 2026
589adb7
refactor(cache): per-profile cache directory under v1/<profile>
Zious11 Apr 25, 2026
dcde80e
refactor(client): JiraClient consumes active profile
Zious11 Apr 25, 2026
43002d9
refactor(team): use active profile for url/cloud_id/org_id
Zious11 Apr 25, 2026
f515134
feat(cli): add --profile global flag with precedence chain
Zious11 Apr 25, 2026
d759161
feat(auth): add jr auth switch subcommand
Zious11 Apr 25, 2026
2a92bd3
feat(auth): add jr auth list subcommand
Zious11 Apr 25, 2026
ce0975f
feat(auth): jr auth login supports --profile and --url
Zious11 Apr 25, 2026
d39d2dd
feat(auth): jr auth status/refresh/logout support --profile
Zious11 Apr 25, 2026
f1a9e69
feat(auth): add jr auth remove subcommand
Zious11 Apr 25, 2026
6686e0e
feat(init): multi-profile awareness; add integration tests
Zious11 Apr 25, 2026
c21af27
refactor(config): stop serializing legacy [instance]/[fields] blocks
Zious11 Apr 25, 2026
657544f
docs: document multi-profile auth in README and CLAUDE.md
Zious11 Apr 25, 2026
134e485
fix: address Copilot review on PR #275
Zious11 Apr 25, 2026
c4ac024
fix(auth): chosen_flow + resolve_oauth_scopes read active profile
Zious11 Apr 25, 2026
3e9a543
fix: address Copilot round-2 review on PR #275
Zious11 Apr 25, 2026
9365eb0
fix: address Copilot round-3 review on PR #275
Zious11 Apr 25, 2026
78bf679
fix: address Copilot round-4 review on PR #275
Zious11 Apr 25, 2026
6bdb7a4
fix(config): add load_lenient for jr auth login profile creation
Zious11 Apr 25, 2026
c00f8e9
fix: address Copilot round-5 review on PR #275
Zious11 Apr 25, 2026
71f05de
fix(init): use Config::load_lenient when adding a profile
Zious11 Apr 25, 2026
5de27da
fix: address Copilot round-7 review on PR #275
Zious11 Apr 25, 2026
c5b48a3
fix: address Copilot round-8 review on PR #275
Zious11 Apr 25, 2026
1939c7c
fix: address Copilot round-9 review on PR #275
Zious11 Apr 25, 2026
71f85d9
fix: address Copilot round-10 review on PR #275
Zious11 Apr 25, 2026
65225c3
fix(auth): surface clear errors in jr auth remove
Zious11 Apr 25, 2026
a9c3f40
fix: address Copilot round-12 review on PR #275
Zious11 Apr 25, 2026
65c7ff7
fix: address Copilot round-13 review on PR #275
Zious11 Apr 25, 2026
d606203
fix(auth): recover from partial namespaced + intact legacy OAuth pair
Zious11 Apr 26, 2026
dc02899
fix(auth): promote target as default_profile if unset in login_token/…
Zious11 Apr 26, 2026
1486c89
fix: address Copilot round-16 review on PR #275
Zious11 Apr 26, 2026
2de878c
fix(config): isolate save-back from env overrides during auto-migration
Zious11 Apr 26, 2026
02c2b61
fix: address Copilot round-18 review on PR #275
Zious11 Apr 26, 2026
ef98474
fix: address Copilot round-19 review on PR #275
Zious11 Apr 26, 2026
57a4be7
fix: address Copilot round-20 review on PR #275
Zious11 Apr 26, 2026
c9e3bdb
fix: address Copilot round-21 review on PR #275
Zious11 Apr 26, 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
17 changes: 11 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Single-crate thin client wrapping Jira REST API v3 and Agile REST API directly w
src/
├── main.rs # Entry point, tokio runtime, clap dispatch, Ctrl+C handling
├── cli/ # Clap derive definitions + command handlers
│ ├── mod.rs # CLI enums, global flags (--output, --project, --no-input, --no-color)
│ ├── mod.rs # CLI enums, global flags (--output, --project, --profile, --no-input, --no-color)
│ ├── issue/ # issue commands (split by operation theme)
│ │ ├── mod.rs # dispatch + re-exports
│ │ ├── format.rs # row formatting, headers, points display
Expand All @@ -26,13 +26,13 @@ src/
│ ├── worklog.rs # worklog add/list
│ ├── team.rs # team list (with cache + lazy org discovery)
│ ├── user.rs # user search/list/view (thin wrapper over api/jira/users.rs)
│ ├── auth.rs # auth login (API token default, --oauth for OAuth 2.0), auth status
│ ├── auth.rs # auth login/switch/list/status/refresh/logout/remove. Multi-profile aware via --profile.
│ ├── init.rs # Interactive setup (prefetches org metadata + team cache + story points field)
│ ├── project.rs # project fields (types, priorities, statuses, CMDB fields)
│ └── queue.rs # queue list/view (JSM service desks)
├── api/
│ ├── client.rs # JiraClient — HTTP methods, auth headers, rate limit retry, 429/401 handling
│ ├── auth.rs # OAuth 2.0 flow, API token storage, keychain read/write, token refresh
│ ├── auth.rs # OAuth 2.0 flow + per-profile keychain layout (shared email/api-token/oauth_client_*; namespaced <profile>:oauth-access-token / <profile>:oauth-refresh-token); lazy migration of legacy flat OAuth keys for the "default" profile
│ ├── pagination.rs # Offset-based (most endpoints) + cursor-based (JQL search)
│ ├── rate_limit.rs # Retry-After parsing
│ ├── assets/ # Assets/CMDB API call implementations
Expand All @@ -57,8 +57,8 @@ src/
├── types/assets/ # Serde structs for Assets API responses (AssetObject, ConnectedTicket, LinkedAsset, etc.)
├── types/jira/ # Serde structs for API responses (Issue, Board, Sprint, User, Team, etc.)
├── types/jsm/ # Serde structs for JSM API responses (ServiceDesk, Queue, etc.)
├── cache.rs # XDG cache (~/.cache/jr/) — team list, project meta, workspace ID (all 7-day TTL)
├── config.rs # Global (~/.config/jr/config.toml) + per-project (.jr.toml), figment layering
├── cache.rs # Per-profile XDG cache (~/.cache/jr/v1/<profile>/) — team list, project meta, workspace ID, CMDB fields, object-type attrs, resolutions (all 7-day TTL). Versioned root (`v1/`) lets a future schema bump orphan stale files cleanly.
├── config.rs # Global (~/.config/jr/config.toml) [profiles.<name>] + default_profile + per-project (.jr.toml), figment layering. Auto-migrates legacy [instance]/[fields] shape on first load. Active profile resolved at load via Config::load_with(cli_profile) (cli flag threaded through as a parameter, NOT an env-var seam) > JR_PROFILE env > default_profile field > "default".
├── output.rs # Table (comfy-table) and JSON formatting
├── adf.rs # Atlassian Document Format: text→ADF, markdown→ADF, ADF→text
├── duration.rs # Worklog duration parser (2h, 1h30m, 1d, 1w)
Expand Down Expand Up @@ -121,15 +121,20 @@ When adding a new feature:

## Gotchas

- **Cache format changes:** `~/.cache/jr/cmdb_fields.json` stores `(id, name)` tuples. Old format (ID-only) causes deserialization failure, handled as cache miss. If you change cache structs, old caches auto-expire (7-day TTL) or fail gracefully.
- **Multi-profile boundary:** every cache reader/writer takes `profile: &str` as its first arg. Pass `&config.active_profile_name` from any handler that has `&Config` in scope. Cross-profile cache leakage is a correctness bug, not a UX issue — sandbox vs prod custom-field IDs can differ.
- **Per-profile vs shared OAuth keys:** `email`, `api-token`, `oauth_client_id`, `oauth_client_secret` live under flat keychain keys (account-level, shared across profiles). `oauth-access-token` / `oauth-refresh-token` are namespaced as `<profile>:oauth-*` because they're cloudId-scoped. The `"default"` profile lazy-migrates legacy flat keys on first read; other profiles do not.
- **Cache format changes:** `~/.cache/jr/v1/<profile>/cmdb_fields.json` stores `(id, name)` tuples. Old format (ID-only) causes deserialization failure, handled as cache miss. If you change cache structs, old caches auto-expire (7-day TTL) or fail gracefully. To break compatibility cleanly, bump the cache root from `v1/` to `v2/` — old files orphan harmlessly.
- **`list.rs` is large (~970 lines):** Contains both `handle_list` and `handle_view` plus all JQL composition logic. If modifying, read the full function you're changing — context matters.
- **`aqlFunction()` not `assetsQuery()`:** The Jira Assets JQL function is `aqlFunction()`. It requires the human-readable field **name**, not `cf[ID]` or `customfield_NNNNN`. AQL attribute for object key is `Key` (not `objectKey` — that's the JSON field name).
- **Status category colors are fixed:** `green` = Done, `yellow` = In Progress, `blue-gray` = To Do. These mappings are hardcoded in Jira Cloud across all instances. Used by `--open` filtering.

## AI Agent Notes

- `JR_BASE_URL` env var overrides the configured Jira instance URL (used by tests to inject wiremock)
- `JR_PROFILE` env var overrides the active profile per-call (combine with direnv to scope a repo to a sandbox site)
- `--profile NAME` flag overrides `JR_PROFILE` for one invocation; precedence is flag > env > config > "default"
- `JiraClient::new_for_test(base_url, auth_header)` constructs a client for integration tests
- Test fixtures live in `tests/common/fixtures.rs`
- Keyring round-trip tests are gated behind `JR_RUN_KEYRING_TESTS=1` + `#[ignore]` because Linux CI may lack secret-service
- All interactive prompts have non-interactive flag equivalents for AI agent usage
- `--output json` on write operations returns structured data (e.g., `{"key": "FOO-123"}`)
62 changes: 46 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,14 @@ jr issue comment JSM-42 "customer is on the paid plan — prioritizing" --intern

| Command | Description |
|---------|-------------|
| `jr init` | Configure Jira instance and authenticate |
| `jr auth login` | Authenticate with API token (default) or `--oauth` for OAuth 2.0. Non-interactive: `--email`/`--token` or `JR_EMAIL`/`JR_API_TOKEN`; `--client-id`/`--client-secret` or `JR_OAUTH_CLIENT_ID`/`JR_OAUTH_CLIENT_SECRET` for OAuth |
| `jr auth refresh` | Clear stored credentials and re-run login (same flags/env vars as `auth login`) |
| `jr auth status` | Show authentication status |
| `jr init` | Configure Jira instance and authenticate (prompts to add another profile if any are already configured) |
| `jr auth login` | Authenticate with API token (default) or `--oauth` for OAuth 2.0. `--profile NAME` targets a specific profile (creates if absent); `--url URL` sets the Jira instance URL when creating. Non-interactive: `--email`/`--token` or `JR_EMAIL`/`JR_API_TOKEN`; `--client-id`/`--client-secret` or `JR_OAUTH_CLIENT_ID`/`JR_OAUTH_CLIENT_SECRET` for OAuth |
| `jr auth switch <NAME>` | Set the default profile in `config.toml`. Errors if `NAME` doesn't exist |
| `jr auth list` | List configured profiles (table or JSON via `--output`); active profile marked with `*` |
| `jr auth status` | Show authentication status for the active profile, or `--profile NAME` for another |
| `jr auth refresh` | Refresh credentials for the active profile (or `--profile NAME`); same flags/env vars as `auth login` |
| `jr auth logout` | Clear OAuth tokens for the active profile (or `--profile NAME`); shared API token NOT touched |
| `jr auth remove <NAME>` | Permanently delete a profile (config entry + cache + per-profile OAuth tokens). Cannot remove the active profile |
| `jr me` | Show current user info |
| `jr issue list` | List issues (`--assignee`, `--reporter`, `--recent`, `--status`, `--open`, `--team`, `--asset KEY`, `--jql`, `--limit`/`--all`, `--points`, `--assets`) |
| `jr issue view KEY` | View issue details (per-field asset rows, enriched JSON, story points) |
Expand Down Expand Up @@ -202,6 +206,7 @@ jr issue comment JSM-42 "customer is on the paid plan — prioritizing" --intern
|------|-------------|
| `--output json\|table` | Output format (default: table) |
| `--project FOO` | Override project key |
| `--profile NAME` | Override the active profile for this invocation (precedence: this flag > `JR_PROFILE` env > `default_profile` in config > `"default"`) |
| `--no-color` | Disable colored output (also respects `NO_COLOR` env) |
| `--no-input` | Disable interactive prompts (auto-enabled in pipes/scripts) |
| `--verbose` | Show HTTP request/response details |
Expand All @@ -215,35 +220,59 @@ jr issue comment JSM-42 "customer is on the paid plan — prioritizing" --intern
# Per-project config (in your repo root)
.jr.toml

# Team cache (disposable, 7-day TTL)
~/.cache/jr/teams.json
# Per-profile cache (disposable, 7-day TTL)
~/.cache/jr/v1/<profile>/teams.json
```

**Global config:**
**Global config (multi-profile shape):**
```toml
[instance]
default_profile = "default"

[profiles.default]
url = "https://yourorg.atlassian.net"
auth_method = "api_token" # or "oauth"
# Optional: override the OAuth 2.0 scope list when auth_method = "oauth".
# Must match what your app in the Atlassian Developer Console has
# configured. Classic and granular scopes CANNOT mix in one request, and
# "offline_access" is required for refresh tokens to be issued. If unset,
# jr uses Atlassian's recommended classic scopes.
# oauth_scopes = "read:issue:jira write:issue:jira write:comment:jira read:jira-user offline_access"
# cloud_id, org_id, oauth_scopes, team_field_id, story_points_field_id
# are auto-discovered during `jr init` / `jr auth login --oauth` and
# populated here per profile.
# oauth_scopes = "read:issue:jira write:issue:jira ... offline_access"

[profiles.sandbox]
url = "https://yourorg-sandbox.atlassian.net"
auth_method = "api_token"
# Sandbox sites usually mirror production custom-field IDs, but `jr` stores
# them per profile so divergence doesn't silently corrupt cached lookups.

[defaults]
output = "table"
```

Switching between profiles:

[fields]
story_points_field_id = "customfield_XXXXX" # auto-discovered during init
```bash
jr auth switch sandbox # persistent — writes default_profile in config.toml
jr --profile sandbox issue list # one-shot — overrides for this call only
JR_PROFILE=sandbox jr issue list # session-scoped (works well with direnv)
```

A single classic Atlassian API token authenticates the same user against
any Atlassian Cloud site, so `email` + `api-token` are stored once in the
OS keychain and shared by all `api_token` profiles. OAuth tokens are
cloudId-scoped and stored per profile.

**Per-project config:**
```toml
project = "FOO"
board_id = 42
```

**Migrating from single-instance configs:** the first run after upgrading
auto-migrates a legacy `[instance]`+`[fields]` config to the new
`[profiles.default]` shape (one stderr notice; idempotent). OAuth tokens
in the OS keychain lazy-migrate from flat keys (`oauth-access-token`) to
namespaced keys (`default:oauth-access-token`) on first authenticated
read. Old cache files at `~/.cache/jr/*.json` orphan harmlessly when the
new layout starts using `~/.cache/jr/v1/<profile>/`.

## Scripting & AI Agents

`jr` is designed to be used by scripts and AI coding agents:
Expand All @@ -255,6 +284,7 @@ board_id = 42
- State-changing commands are idempotent (exit 0 if already in target state)
- Structured exit codes (see [Exit Codes](#exit-codes) table)
- `auth login` / `auth refresh` accept credentials via flags (`--email`, `--token`, `--client-id`, `--client-secret`) or env vars (`JR_EMAIL`, `JR_API_TOKEN`, `JR_OAUTH_CLIENT_ID`, `JR_OAUTH_CLIENT_SECRET`) — no TTY required. Prefer env vars for secrets.
- `--profile NAME` flag and `JR_PROFILE` env var let agents target a specific profile per-call without mutating the user's `default_profile`. Combined with direnv (`echo 'export JR_PROFILE=sandbox' >> .envrc`), a repo can scope all `jr` calls to a sandbox site automatically.

```bash
# AI agent workflow example
Expand Down
Loading