-
Notifications
You must be signed in to change notification settings - Fork 1
feat: multi-profile auth via --profile flag #126
Description
Summary
Add multi-profile support so users working across multiple Linear workspaces can store multiple API tokens and switch between them with --profile <name>. Profiles are simply different ~/.linear_api_token_* files — no config files, no TOML, no new dependencies. lineark stays config-free.
Inspired by #115 (thanks @lightstrike for the idea and effort). This issue captures the design we want to implement instead — same goal, different approach that preserves lineark's zero-config philosophy.
Design Decisions
1. Profile = named token file
Profiles are just ~/.linear_api_token_{name} files. No config file format, no parsing, no new dependencies.
| Profile | Token file |
|---|---|
| (default) | ~/.linear_api_token |
work |
~/.linear_api_token_work |
banana |
~/.linear_api_token_banana |
Rationale: lineark's design principle is "zero config for existing Linear users." A single token file already works today — multiple profiles are just more files following the same pattern. This avoids introducing TOML parsing, XDG config directories, or any new dependency.
2. --profile flag on CLI
lineark --profile work teams list # uses ~/.linear_api_token_work
lineark teams list # uses default auth chain (env → ~/.linear_api_token)--profile is a global flag (#[arg(long, global = true)]), same as --api-token and --format.
3. Auth precedence — --profile is an intentional override
When --profile is specified, it skips the env var and goes straight to the named file. The user explicitly asked for a specific profile — the env var should not silently win.
Full precedence chain:
With --profile:
1. --api-token flag (but conflicts_with --profile, so this is a hard error)
2. ~/.linear_api_token_{profile} file
Without --profile:
1. --api-token flag
2. $LINEAR_API_TOKEN env var
3. ~/.linear_api_token file
Rationale: --profile work means "I want the work token." If $LINEAR_API_TOKEN is also set, the user's intent is clear — they want the profile, not the env var. Letting the env var win would be surprising.
4. --api-token + --profile = hard error
These flags are mutually exclusive via clap's conflicts_with. If both are provided, clap errors before the program runs.
Rationale: Silent precedence creates confusion. If someone passes both, it's a mistake — better to catch it immediately. The codebase already uses conflicts_with in other commands (cycles.rs, labels.rs).
5. SDK becomes path-agnostic (breaking change)
The SDK currently bakes in ~/.linear_api_token via Client::auto() and Client::from_file(). This is a filesystem convention that belongs in the CLI, not the SDK. Library consumers should be able to read tokens from any path.
Remove from SDK:
Client::auto()— bakes in path conventionClient::from_file()— bakes in~/.linear_api_tokenauth::auto_token()— combines env + hardcoded pathauth::token_from_file()(no-arg version) — hardcoded path
Keep/add in SDK:
Client::from_token(token: impl Into<String>)— unchangedClient::from_env()— reads$LINEAR_API_TOKEN, unchangedClient::from_token_file(path: &Path)— new, reads token from any file path (read, trim, good error messages with path included)auth::token_from_env()— unchangedauth::token_from_file(path: &Path)— changed signature, accepts any path instead of hardcoding~/.linear_api_token
Rationale: The SDK is a library. Library consumers may store tokens wherever they want. The ~/.linear_api_token convention is a CLI UX decision, not a library concern. This also cleanly separates profile logic: the CLI owns the naming convention, the SDK just reads files.
This is a breaking change. Current version → next major version.
6. CLI owns the convention
The CLI's client resolution in main.rs becomes:
let home = home::home_dir().expect("could not determine home directory");
let client = match (&cli.api_token, &cli.profile) {
(Some(_), Some(_)) => unreachable!(), // clap conflicts_with prevents this
(Some(token), None) => Client::from_token(token),
(None, Some(profile)) => {
Client::from_token_file(&home.join(format!(".linear_api_token_{profile}")))
}
(None, None) => {
Client::from_env()
.or_else(|_| Client::from_token_file(&home.join(".linear_api_token")))
}
};7. Profile discovery in lineark usage
lineark usage currently shows hints like (set) / (found) for env var and token file. Update it to discover and list available profiles.
Discovery: glob ~/.linear_api_token_* (note the underscore — this avoids matching backup files like .linear_api_token.bak). Parse the suffix after _ as the profile name.
Filter: strip test from the displayed list. The _test file is for CI/online tests and would confuse users.
Output example:
AUTH (in precedence order):
1. --api-token flag
2. $LINEAR_API_TOKEN env var (set)
3. ~/.linear_api_token file (found)
found profiles: "default", "work", "banana". switch with --profile <name>
"default" appears when ~/.linear_api_token exists (no suffix = default profile).
8. Error UX when profile file is missing
When --profile work is specified but ~/.linear_api_token_work doesn't exist:
Error: Profile "work" not found. Available profiles: "default", "banana".
Create it with:
echo "lin_api_..." > ~/.linear_api_token_work
If no profiles exist at all:
Error: Profile "work" not found. No profiles found.
Create it with:
echo "lin_api_..." > ~/.linear_api_token_work
Rationale: Actionable error messages. The user knows exactly what to do and can see what's available.
9. Refactor test-utils to use SDK
lineark-test-utils currently hardcodes ~/.linear_api_token_test with its own file-reading logic. Refactor to use the SDK's token_from_file():
// Before (hardcoded path + own file reading)
pub fn test_token() -> String {
let path = home::home_dir().unwrap().join(".linear_api_token_test");
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("could not read {}: {}", path.display(), e))
.trim()
.to_string()
}
// After (uses SDK)
pub fn test_token() -> String {
let path = home::home_dir().unwrap().join(".linear_api_token_test");
lineark_sdk::auth::token_from_file(&path)
.unwrap_or_else(|e| panic!("could not read test token: {}", e))
}Rationale: Single source of truth for reading token files. The SDK owns file reading (read, trim, error messages), consumers just provide a path.
10. No LINEAR_PROFILE env var
The PR proposed LINEAR_PROFILE env var support. We're not doing this. Profile is not a secret — it doesn't need the env var treatment that tokens get (avoiding ps aux leaks). The --profile flag is sufficient.
Implementation Plan
Step 1: SDK changes (crates/lineark-sdk/)
src/auth.rs:
- Change
token_from_file()signature totoken_from_file(path: &Path) -> Result<String, LinearError> - Remove
auto_token()(it combined env + hardcoded path) - Remove
token_file_path()helper (hardcoded~/.linear_api_token) - Keep
token_from_env()unchanged - Update unit tests — remove tests for removed functions, update tests for new
token_from_file(&Path)signature
src/client.rs:
- Remove
Client::auto() - Remove
Client::from_file() - Add
Client::from_token_file(path: &Path) -> Result<Self, LinearError> - Keep
Client::from_token()andClient::from_env()unchanged
src/lib.rs:
- Update re-exports if needed
src/helpers.rs:
- Update doc examples that reference
Client::auto()
README.md:
- Update auth documentation table — remove
autorow, addfrom_token_filerow
Step 2: CLI changes (crates/lineark/)
src/main.rs:
- Add
--profileflag toClistruct withconflicts_with = "api_token" - Update client resolution to the three-branch match (api_token / profile / default)
- Add profile discovery helper (glob + filter) for error messages
src/commands/usage.rs:
- Add profile discovery: glob
~/.linear_api_token_*, striptest, display found profiles - Update AUTH section to mention
--profile
README.md:
- Add multi-profile section with setup examples
Step 3: test-utils refactor (crates/lineark-test-utils/)
src/token.rs:
- Replace hardcoded file reading with
lineark_sdk::auth::token_from_file(&path) - Keep
test_token()andno_online_test_token()public API unchanged
Step 4: Tests
SDK unit tests (crates/lineark-sdk/src/auth.rs):
token_from_filewith valid file (tempfile)token_from_filewith missing file (good error message with path)token_from_filewith empty filetoken_from_filetrims whitespacetoken_from_envunchanged tests
CLI offline tests (crates/lineark/tests/offline.rs):
--profile workwith valid~/.linear_api_token_work(use tempdir + HOME override)--profile workwith missing file → error message includes profile name, available profiles, creation hint--profile+--api-token→ clap error--helpshows--profileflaglineark usageshows discovered profiles- Default (no flag) still works with
~/.linear_api_token - Default (no flag) still works with
$LINEAR_API_TOKENenv var
Step 5: Documentation
- Update
lineark usageoutput - Update CLI README
- Update SDK README
- Run
/update-docsbefore merging
Out of Scope
LINEAR_PROFILEenv var — not needed,--profileflag suffices- TOML config file — lineark stays config-free
tomldependency — not needed- Profile switching within a session — one profile per invocation
- SDK-level profile awareness — SDK is path-agnostic, CLI owns conventions
Version Impact
This is a breaking change to the SDK's public API (Client::auto() and Client::from_file() removed). Bump to next major version.
References
- PR feat: multi-profile auth via ~/.config/lineark/config.toml #115 by @lightstrike — original multi-profile proposal (TOML-based approach)
- Discussion in feat: multi-profile auth via ~/.config/lineark/config.toml #115 comments — rationale for config-free approach