feat(config): named configurations for multi-instance support #19

Merged
David merged 1 commit from feat/configurations-multi-instance into main 2026-05-14 16:25:43 +02:00
Owner

Summary

Adds named configurations for multi-instance support, following the gcloud pattern. Each YouTrack instance is a separate profile; commands target the active one by default and any individual command can override it without changing what's active.

Disk layout

  • $XDG_CONFIG_HOME/youtrack-cli/configurations/config_<name>.yaml per profile (mode 0600).
  • $XDG_CONFIG_HOME/youtrack-cli/active_config single-line pointer to the active profile (mode 0600).
  • Directory created 0700 (unchanged).

Resolution order

Highest priority first:

  1. --configuration <name> global flag
  2. $YOUTRACK_CLI_ACTIVE_CONFIG env var
  3. The active_config pointer file

--config <path> still works and bypasses the named-configuration store entirely, reading a single YAML file. Used by tests and as an escape hatch.

New subcommands

yt config configurations create <name> [--no-activate]
yt config configurations activate <name>
yt config configurations list [--json]
yt config configurations describe <name> [--no-mask]
yt config configurations delete <name>   # refuses to delete the active one

First-run / friendly UX

yt auth login --base-url <url> on a fresh install (no active_config pointer, no profiles) auto-creates and activates a profile named default. So getting started is still one command. With an explicit --configuration or an already-active profile, login writes to that profile and never silently rewires which one is active.

Migration of the pre-feature single config.yaml

NOT auto-migrated, per the explicit decision on this PR. Existing users hitting a read command will see:

no active configuration. Run `yt auth login --base-url <url>` to create a `default` one, or `yt config configurations create <name>`

The legacy ~/.config/youtrack-cli/config.yaml is left in place; users can either rerun auth login (clean slate, creates a new default) or manually move the file to configurations/config_default.yaml and write default into active_config.

Plumbing

  • src/config.rs -> src/config/ (with mod.rs keeping the Config struct + filesystem helpers and store.rs adding the named-configuration layer).
  • New Config::load_active(path_override, configuration_override) -> (Config, ConfigSource). Every command's run now takes a configuration_override: Option<&str> and funnels through it.
  • config set writes back via ConfigSource::save_to, so it lands in whichever file the load came from.
  • Config::default_path() is removed: there is no single canonical config path anymore.

Verification

  • cargo fmt --check clean.
  • cargo clippy --all-targets -- -D warnings clean.
  • cargo test --all-targets: 160 tests pass (24 new on this branch).
  • yt --help shows --configuration <CONFIGURATION> + env var.
  • yt config configurations --help shows the 5-verb surface.

Coverage on this PR: gcloud-style name validation (empty / uppercase / underscore / leading-digit rejected), full resolution priority order, pure resolver returns None when nothing set, CRUD on the store (create + duplicate refusal, activate + activate-unknown error, delete-active refusal, delete-inactive success), 0600 mode on the active pointer file, list / describe end-to-end including masked + unmasked token output.

Test plan

  • On a fresh install: yt auth login --base-url https://<instance> creates ~/.config/youtrack-cli/configurations/config_default.yaml, writes default into active_config, and the profile is usable immediately.
  • yt config configurations create prod creates a second profile and switches to it.
  • yt --configuration default list uses the default profile for one command without touching active_config.
  • yt config configurations list shows both profiles with * on the active one.
  • yt config configurations delete <active> is refused; deleting the other one succeeds.
  • On a box with the pre-feature ~/.config/youtrack-cli/config.yaml but no configurations/ dir, the first yt list errors with the migration hint and the legacy file is untouched.
## Summary Adds named configurations for multi-instance support, following the `gcloud` pattern. Each YouTrack instance is a separate profile; commands target the active one by default and any individual command can override it without changing what's active. ## Disk layout - `$XDG_CONFIG_HOME/youtrack-cli/configurations/config_<name>.yaml` per profile (mode 0600). - `$XDG_CONFIG_HOME/youtrack-cli/active_config` single-line pointer to the active profile (mode 0600). - Directory created 0700 (unchanged). ## Resolution order Highest priority first: 1. `--configuration <name>` global flag 2. `$YOUTRACK_CLI_ACTIVE_CONFIG` env var 3. The `active_config` pointer file `--config <path>` still works and bypasses the named-configuration store entirely, reading a single YAML file. Used by tests and as an escape hatch. ## New subcommands ``` yt config configurations create <name> [--no-activate] yt config configurations activate <name> yt config configurations list [--json] yt config configurations describe <name> [--no-mask] yt config configurations delete <name> # refuses to delete the active one ``` ## First-run / friendly UX `yt auth login --base-url <url>` on a fresh install (no `active_config` pointer, no profiles) auto-creates and activates a profile named `default`. So getting started is still one command. With an explicit `--configuration` or an already-active profile, login writes to that profile and never silently rewires which one is active. ## Migration of the pre-feature single `config.yaml` NOT auto-migrated, per the explicit decision on this PR. Existing users hitting a read command will see: ``` no active configuration. Run `yt auth login --base-url <url>` to create a `default` one, or `yt config configurations create <name>` ``` The legacy `~/.config/youtrack-cli/config.yaml` is left in place; users can either rerun `auth login` (clean slate, creates a new `default`) or manually move the file to `configurations/config_default.yaml` and write `default` into `active_config`. ## Plumbing - `src/config.rs` -> `src/config/` (with `mod.rs` keeping the `Config` struct + filesystem helpers and `store.rs` adding the named-configuration layer). - New `Config::load_active(path_override, configuration_override) -> (Config, ConfigSource)`. Every command's `run` now takes a `configuration_override: Option<&str>` and funnels through it. - `config set` writes back via `ConfigSource::save_to`, so it lands in whichever file the load came from. - `Config::default_path()` is removed: there is no single canonical config path anymore. ## Verification - `cargo fmt --check` clean. - `cargo clippy --all-targets -- -D warnings` clean. - `cargo test --all-targets`: 160 tests pass (24 new on this branch). - `yt --help` shows `--configuration <CONFIGURATION>` + env var. - `yt config configurations --help` shows the 5-verb surface. Coverage on this PR: gcloud-style name validation (empty / uppercase / underscore / leading-digit rejected), full resolution priority order, pure resolver returns `None` when nothing set, CRUD on the store (create + duplicate refusal, activate + activate-unknown error, delete-active refusal, delete-inactive success), 0600 mode on the active pointer file, list / describe end-to-end including masked + unmasked token output. ## Test plan - [ ] On a fresh install: `yt auth login --base-url https://<instance>` creates `~/.config/youtrack-cli/configurations/config_default.yaml`, writes `default` into `active_config`, and the profile is usable immediately. - [ ] `yt config configurations create prod` creates a second profile and switches to it. - [ ] `yt --configuration default list` uses the `default` profile for one command without touching `active_config`. - [ ] `yt config configurations list` shows both profiles with `*` on the active one. - [ ] `yt config configurations delete <active>` is refused; deleting the other one succeeds. - [ ] On a box with the pre-feature `~/.config/youtrack-cli/config.yaml` but no `configurations/` dir, the first `yt list` errors with the migration hint and the legacy file is untouched.
feat(config): named configurations for multi-instance support
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 9s
Create release / Create release from merged PR (pull_request) Has been skipped
4123be673c
Follows the `gcloud` pattern: each YouTrack instance is a named profile, switchable via a global flag, an env var, or an on-disk pointer. The legacy single `config.yaml` is no longer read.

Storage:
- `$XDG_CONFIG_HOME/youtrack-cli/configurations/config_<name>.yaml` per profile (0600).
- `$XDG_CONFIG_HOME/youtrack-cli/active_config` single-line pointer to the active profile (0600).
- Directory created 0700 like before.

Resolution (highest first): `--configuration <name>` global flag, `$YOUTRACK_CLI_ACTIVE_CONFIG` env var, the `active_config` pointer file. `--config <path>` still bypasses the whole store and reads a single file directly (used by tests and as an escape hatch).

New subcommands under `yt config configurations`:
- `create <name> [--no-activate]`: create a new profile, activating it by default.
- `activate <name>`: switch the active profile.
- `list [--json]`: show all profiles, marking the active one.
- `describe <name> [--no-mask]`: print a profile's contents (token masked unless `--no-mask`).
- `delete <name>`: remove a profile; refuses to delete the active one.

`yt auth login` is special-cased for first-run UX: with no explicit `--configuration` and no `active_config` pointer on disk, it auto-creates and activates a profile named `default`, so a fresh install needs exactly one command. When a `--configuration` is passed or the active pointer already names a profile, login writes to that profile and never silently rewires which one is active.

Migration: existing users with `~/.config/youtrack-cli/config.yaml` are NOT auto-migrated (per the explicit decision on this PR). Their next read command errors with a hint to run `yt auth login` or `yt config configurations create <name>`. The legacy file is left untouched.

Plumbing:
- `src/config.rs` is now `src/config/`, with `mod.rs` keeping the `Config` struct + filesystem helpers and `store.rs` adding the named-configuration layer (`Configurations` type + pure `validate_configuration_name` / `resolve_configuration_name` helpers).
- Each top-level command's `run` gained a `configuration_override: Option<&str>` argument; they all funnel through new `Config::load_active(path_override, configuration_override) -> (Config, ConfigSource)`.
- `config set` writes through `ConfigSource::save_to`, so it lands in whichever file the load came from.
- `Config::default_path()` is removed: there is no longer a single canonical config path.

Tests cover: profile name validation (gcloud rules, including empty / uppercase / underscore / leading-digit rejection), full priority order for resolution (flag > env > pointer), pure resolver returning None when nothing is set, store CRUD (create + duplicate refusal, activate + activate-unknown error, delete-active refusal, delete-inactive success), 0600 mode on the active pointer file, configurations list / describe (masked + unmasked) end-to-end. 160 tests pass; clippy clean; fmt clean.

README and CLAUDE.md updated with the new storage layout, resolution order, and a worked example covering create / activate / per-command override.
David merged commit 4dc0b30686 into main 2026-05-14 16:25:43 +02:00
David deleted branch feat/configurations-multi-instance 2026-05-14 16:25:43 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pandoras-box/youtrack-cli!19
No description provided.