feat(config): per-instance config files keyed on URL subdomain #26

Merged
David merged 1 commit from feat/per-instance-config into main 2026-05-14 18:12:10 +02:00
Owner

Summary

Redesigns the multi-instance config layout that landed in PR #19 so adding a new YouTrack instance never overwrites an existing one. Each instance gets its own YAML file keyed on the URL subdomain:

~/.config/youtrack-cli/
  config-niceguyit.yml       # https://niceguyit.myjetbrains.com
  config-prod.yml            # https://prod.youtrack.cloud
  config.yml                 # { default: niceguyit }

auth login derives the instance name from the first hostname label of --base-url. Logging in to a new URL writes a new file. The default: pointer is set on the first login and is NOT silently changed afterwards: a second auth login leaves the default alone.

Resolution order

Highest priority first:

  1. --instance <name> (renamed from --configuration)
  2. $YOUTRACK_CLI_INSTANCE (renamed from $YOUTRACK_CLI_ACTIVE_CONFIG)
  3. The default: field in config.yml
  4. --config <path> bypasses everything and reads a single file directly (mostly for tests).

CLI surface changes

yt auth login --base-url https://niceguyit.myjetbrains.com   # creates config-niceguyit.yml; sets default: niceguyit
yt auth login --base-url https://prod.youtrack.cloud         # creates config-prod.yml; default still niceguyit
yt --instance prod list                                       # per-command override
# Switch default: edit ~/.config/youtrack-cli/config.yml

The config configurations {create,activate,list,describe,delete} subcommand group is dropped per the design call: auth login is the creation path; --instance is the per-command override; switching the default is vim ~/.config/youtrack-cli/config.yml. The list / describe / delete verbs are listed in TODO for a future yt auth list if they earn their keep.

Rename map

Old New
--configuration <name> --instance <name>
$YOUTRACK_CLI_ACTIVE_CONFIG $YOUTRACK_CLI_INSTANCE
Configurations (store) Instances
src/config/store.rs src/config/instances.rs
ConfigSource::Configuration(s) ConfigSource::Instance(s)
configuration_override: Option<&str> instance_override: Option<&str>
configurations/config_<name>.yaml config-<instance>.yml
active_config (single-line text) config.yml with default: <name>

Migration of the pre-feature layout

Not auto-migrated. Existing users with configurations/config_<name>.yaml + active_config see the same "no instance configured" hint a fresh-install user sees. Manual migration:

mv ~/.config/youtrack-cli/configurations/config_<name>.yaml ~/.config/youtrack-cli/config-<name>.yml
printf 'default: <name>\n' > ~/.config/youtrack-cli/config.yml

URL parsing details

Uses url::Url::host() so the typed enum (Domain / Ipv4 / Ipv6) drives the IP-vs-domain decision. Rejects IPv4 literals, bracketed IPv6 literals, and single-label hosts (localhost, raw IPs) with a message telling the user to pass --instance <name> explicitly. Derived names are lowercased and validated against the same gcloud rules already in place (lowercase letter prefix, then lowercase letters / digits / hyphens).

Verification

  • cargo fmt --check clean.
  • cargo clippy --all-targets -- -D warnings clean.
  • cargo test --all-targets: 178 tests pass (8 store tests removed for dropped methods, 10 new tests for URL derivation + pointer round-trip + name validation, 6 fewer overall because the deleted config configurations subcommand test module went with it).
  • yt --help shows --instance; no config configurations subgroup remains.

Test plan

  • Fresh install: yt auth login --base-url https://niceguyit.myjetbrains.com. Confirm ~/.config/youtrack-cli/config-niceguyit.yml exists with the token and ~/.config/youtrack-cli/config.yml contains default: niceguyit.
  • Add a second instance: yt auth login --base-url https://prod.youtrack.cloud. Confirm ~/.config/youtrack-cli/config-prod.yml is new, config-niceguyit.yml is unchanged, and config.yml still says default: niceguyit.
  • yt --instance prod list reads from config-prod.yml without touching the default.
  • vim ~/.config/youtrack-cli/config.yml, change default: to prod, then yt list uses prod.
  • yt auth login --base-url https://localhost:8080 fails with the "single-label" hint and tells the user to pass --instance <name>.
  • yt auth login --base-url https://localhost:8080 --instance dev succeeds and writes config-dev.yml.
## Summary Redesigns the multi-instance config layout that landed in PR #19 so adding a new YouTrack instance never overwrites an existing one. Each instance gets its own YAML file keyed on the URL subdomain: ``` ~/.config/youtrack-cli/ config-niceguyit.yml # https://niceguyit.myjetbrains.com config-prod.yml # https://prod.youtrack.cloud config.yml # { default: niceguyit } ``` `auth login` derives the instance name from the first hostname label of `--base-url`. Logging in to a new URL writes a new file. The `default:` pointer is set on the first login and is NOT silently changed afterwards: a second `auth login` leaves the default alone. ## Resolution order Highest priority first: 1. `--instance <name>` (renamed from `--configuration`) 2. `$YOUTRACK_CLI_INSTANCE` (renamed from `$YOUTRACK_CLI_ACTIVE_CONFIG`) 3. The `default:` field in `config.yml` 4. `--config <path>` bypasses everything and reads a single file directly (mostly for tests). ## CLI surface changes ``` yt auth login --base-url https://niceguyit.myjetbrains.com # creates config-niceguyit.yml; sets default: niceguyit yt auth login --base-url https://prod.youtrack.cloud # creates config-prod.yml; default still niceguyit yt --instance prod list # per-command override # Switch default: edit ~/.config/youtrack-cli/config.yml ``` The `config configurations {create,activate,list,describe,delete}` subcommand group is dropped per the design call: `auth login` is the creation path; `--instance` is the per-command override; switching the default is `vim ~/.config/youtrack-cli/config.yml`. The list / describe / delete verbs are listed in TODO for a future `yt auth list` if they earn their keep. ## Rename map | Old | New | |---|---| | `--configuration <name>` | `--instance <name>` | | `$YOUTRACK_CLI_ACTIVE_CONFIG` | `$YOUTRACK_CLI_INSTANCE` | | `Configurations` (store) | `Instances` | | `src/config/store.rs` | `src/config/instances.rs` | | `ConfigSource::Configuration(s)` | `ConfigSource::Instance(s)` | | `configuration_override: Option<&str>` | `instance_override: Option<&str>` | | `configurations/config_<name>.yaml` | `config-<instance>.yml` | | `active_config` (single-line text) | `config.yml` with `default: <name>` | ## Migration of the pre-feature layout Not auto-migrated. Existing users with `configurations/config_<name>.yaml` + `active_config` see the same "no instance configured" hint a fresh-install user sees. Manual migration: ``` mv ~/.config/youtrack-cli/configurations/config_<name>.yaml ~/.config/youtrack-cli/config-<name>.yml printf 'default: <name>\n' > ~/.config/youtrack-cli/config.yml ``` ## URL parsing details Uses `url::Url::host()` so the typed enum (`Domain` / `Ipv4` / `Ipv6`) drives the IP-vs-domain decision. Rejects IPv4 literals, bracketed IPv6 literals, and single-label hosts (`localhost`, raw IPs) with a message telling the user to pass `--instance <name>` explicitly. Derived names are lowercased and validated against the same gcloud rules already in place (lowercase letter prefix, then lowercase letters / digits / hyphens). ## Verification - `cargo fmt --check` clean. - `cargo clippy --all-targets -- -D warnings` clean. - `cargo test --all-targets`: 178 tests pass (8 store tests removed for dropped methods, 10 new tests for URL derivation + pointer round-trip + name validation, 6 fewer overall because the deleted `config configurations` subcommand test module went with it). - `yt --help` shows `--instance`; no `config configurations` subgroup remains. ## Test plan - [ ] Fresh install: `yt auth login --base-url https://niceguyit.myjetbrains.com`. Confirm `~/.config/youtrack-cli/config-niceguyit.yml` exists with the token and `~/.config/youtrack-cli/config.yml` contains `default: niceguyit`. - [ ] Add a second instance: `yt auth login --base-url https://prod.youtrack.cloud`. Confirm `~/.config/youtrack-cli/config-prod.yml` is new, `config-niceguyit.yml` is unchanged, and `config.yml` still says `default: niceguyit`. - [ ] `yt --instance prod list` reads from `config-prod.yml` without touching the default. - [ ] `vim ~/.config/youtrack-cli/config.yml`, change `default:` to `prod`, then `yt list` uses prod. - [ ] `yt auth login --base-url https://localhost:8080` fails with the "single-label" hint and tells the user to pass `--instance <name>`. - [ ] `yt auth login --base-url https://localhost:8080 --instance dev` succeeds and writes `config-dev.yml`.
feat(config): per-instance config files keyed on URL subdomain
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 21s
Create release / Create release from merged PR (pull_request) Has been skipped
eff31ad2d9
Redesigns the multi-instance config layout that landed in PR #19. Each YouTrack instance now gets its own YAML file under `$XDG_CONFIG_HOME/youtrack-cli/`:

  config-niceguyit.yml       # https://niceguyit.myjetbrains.com
  config-prod.yml            # https://prod.youtrack.cloud
  config.yml                 # { default: niceguyit }

The instance name is derived from the first hostname label of `--base-url`. `auth login` to a NEW URL writes a NEW file - it does not silently overwrite the existing instance. The `default:` pointer is set on the first login and never silently changed afterwards: adding a second instance leaves you on the same default until you switch by editing `config.yml` (or pass `--instance <name>`).

Pointer format is a single-key YAML document, parseable with `serde_yml`:

  default: niceguyit

Resolution order (highest first): `--instance <name>` global flag (renamed from `--configuration`), `$YOUTRACK_CLI_INSTANCE` env var (renamed from `$YOUTRACK_CLI_ACTIVE_CONFIG`), the `default:` field in `config.yml`. `--config <path>` still bypasses the store for tests / escape hatch.

URL parsing uses `url::Url::host()` so it correctly rejects both IPv4 (`192.168.1.10`) and bracketed IPv6 (`[::1]`) literals and single-label hosts (`localhost`); those cases tell the user to pass `--instance <name>` explicitly. The derived name is lowercased and validated against the gcloud-style rules already in place (leading lowercase letter, then lowercase letters / digits / hyphens).

Drops the `config configurations {create,activate,list,describe,delete}` subcommand group entirely per the design call. `auth login` is the creation path; `--instance <name>` is the per-command override; switching the default is `vim ~/.config/youtrack-cli/config.yml`. The list / describe verbs are listed in TODO as a future `yt auth list` / `yt auth show` if they prove worth shipping.

Migration: existing users with `~/.config/youtrack-cli/configurations/config_<name>.yaml` + `active_config` are not auto-migrated. The next read command errors with the same "no instance configured. Run `yt auth login --base-url <url>` to create one." message that fresh-install users see. Manual migration: `mv .../configurations/config_<name>.yaml .../config-<name>.yml`, write `default: <name>` into `config.yml`.

Plumbing rename across the codebase:

  --configuration <name>           ->  --instance <name>
  YOUTRACK_CLI_ACTIVE_CONFIG       ->  YOUTRACK_CLI_INSTANCE
  configuration_override: Option   ->  instance_override: Option
  ConfigSource::Configuration(s)   ->  ConfigSource::Instance(s)
  Configurations                    ->  Instances
  src/config/store.rs               ->  src/config/instances.rs

Tests cover: name validation (typical names + the same invalid-name set as before), subdomain extraction (typical, uppercase lowercased, ports + paths ignored), single-label-host rejection, IPv4 + IPv6 literal rejection, invalid-URL rejection, resolver priority chain (flag > env > pointer + every empty / absent permutation), default pointer round-trip (read/write/empty-file/missing-file), name validation on write_default_pointer, pointer file mode 0o600 on unix. The `auth login` happy paths use the existing wiremock token-verify tests; the prepare_target flow is integration-shaped (touches filesystem at XDG_CONFIG_HOME) and gets manual coverage rather than a wiremock test. 178 tests pass; clippy/fmt clean.

README + CLAUDE.md updated with the new layout, the new flag and env var names, and the explicit "auth login does not overwrite existing instance" guarantee.
David merged commit 2a7edc7234 into main 2026-05-14 18:12:10 +02:00
David deleted branch feat/per-instance-config 2026-05-14 18:12:10 +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!26
No description provided.