diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 07:10:33 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 07:10:33 -0500 |
| commit | 394c75b2f92e2a72390b3417e483849bed13a399 (patch) | |
| tree | a5b92603ecb049381015fb7556f25dcf74fa6c21 /README.org | |
| parent | 4767d16423ff5c87cf1222603c7b8f1b59af8ef2 (diff) | |
| download | pearl-394c75b2f92e2a72390b3417e483849bed13a399.tar.gz pearl-394c75b2f92e2a72390b3417e483849bed13a399.zip | |
feat(accounts): multi-account support with tests
I added a named-account layer for working more than one Linear workspace from one Emacs.
Before this, everything that identified a workspace was a single global: pearl-api-key, pearl-graphql-url, pearl-org-file-path, and the lookup caches. Nothing stopped a work command from running under personal credentials or a work fetch from landing in the personal file, and switching accounts meant re-customizing the key, team, and file by hand and clearing the cache.
pearl-accounts maps a name to a per-workspace plist (credential source, org file, default team, optional endpoint), and pearl-switch-account makes one active. Account state flows through an explicit context rather than mutating globals. Every request snapshots its account at dispatch through pearl--graphql-request-async and re-establishes it around the callbacks. A switch mid-fetch can't bleed into a request already in flight: the result finishes into the account it was dispatched under. I centralized this in the one request primitive, so the leak surface is a single function instead of every call site.
Rendered files carry a #+LINEAR-ACCOUNT marker, and a buffer guard refuses a command run from one account's file while another is active, naming both, so a work edit can't push under personal credentials. An unmarked legacy file lets reads and refreshes through and acquires its marker on the first refresh. Mutations wait until then. Credentials resolve through auth-source, an env var, or an inline literal, and a resolved key is never persisted through Customize or logged. The active account shows in the mode line. A saved query can carry an :account so it refuses to run under the wrong workspace before any lookup.
With pearl-accounts unset, everything behaves exactly as before, off the legacy globals.
Diffstat (limited to 'README.org')
| -rw-r--r-- | README.org | 57 |
1 files changed, 56 insertions, 1 deletions
@@ -22,6 +22,7 @@ Pearl (backronym: *Pearl Edits and Reflects Linear*) brings Linear issues into E - Add and delete your own comments, create issues, delete issues, and open the current issue or view in Linear - Refresh the active view from the source recorded in the file, or refresh one issue at point - Render Linear workflow states as Org TODO keywords +- Work multiple Linear workspaces from one Emacs: named accounts, a safe switch, per-file ownership, and a mode-line indicator - Use one transient dispatcher, =M-x pearl-menu=, for the whole command surface - [[file:TESTING.org][Well-tested]] with isolated ERT files, request fixtures, and coverage support @@ -362,9 +363,11 @@ Most users only need an API key and an output path. The rest are knobs for teams | Variable | Purpose | |-----------------------------+---------------------------------------------------| -| =pearl-api-key= | Linear API key | +| =pearl-api-key= | Linear API key (single-account) | | =pearl-org-file-path= | Active Org output file | | =pearl-default-team-id= | Default team for issue creation | +| =pearl-accounts= | Named accounts for multiple workspaces | +| =pearl-default-account= | Account made active at first need | | =pearl-saved-queries= | Named local issue queries | | =pearl-max-issue-pages= | Pagination cap, 100 issues per page | | =pearl-request-timeout= | Synchronous request timeout in seconds | @@ -391,6 +394,58 @@ Common shapes: For per-URL routing (e.g. "Linear goes to the work-account browser, everything else to my personal one"), set =browse-url-handlers= with a list of =(REGEXP . FUNCTION)= pairs. +** Multiple accounts +:PROPERTIES: +:CUSTOM_ID: multiple-accounts +:END: + +If you work more than one Linear workspace — say a work account and a personal one — set =pearl-accounts= and switch between them with =M-x pearl-switch-account=. Each account names where its key is found, which Org file holds its issues, and (optionally) a default team: + +#+begin_src elisp +(setq pearl-accounts + '(("work" :api-key-source (:auth-source :host "api.linear.app" :user "work") + :org-file "~/org/work-linear.org" + :default-team-id "TEAM_WORK") + ("personal" :api-key-source (:env "LINEAR_PERSONAL_API_KEY") + :org-file "~/org/personal-linear.org"))) +(setq pearl-default-account "work") +#+end_src + +=:api-key-source= says how the key is *found*, not the key itself. Three forms: + +| Form | Where the key comes from | +|-----------------------------------------+---------------------------------------------------| +| =(:auth-source :host H :user U)= | =~/.authinfo.gpg= (the documented default) | +| =(:env "VAR")= | the named environment variable | +| =(:literal "lin_...")= | the string inline (an escape hatch) | + +The auth-source form keeps the key out of your config. An =~/.authinfo.gpg= line for the example above: + +#+begin_example +machine api.linear.app login work password lin_api_xxxxxxxx +#+end_example + +A resolved key is never written back through Customize and never logged. =pearl-load-api-key-from-env= is a legacy single-account convenience and refuses once =pearl-accounts= is set — put =:api-key-source (:env "...")= in the account instead. + +*** Switching, ownership, and the indicator + +=pearl-switch-account= makes an account active, clears the per-workspace lookup caches so the next fetch resolves against the new workspace, and visits that account's Org file. The active account shows in the mode line as =Pearl[work]=. + +Each rendered file is stamped with =#+LINEAR-ACCOUNT:= naming the workspace that owns it. Pearl refuses to run a command from a file owned by a different account than the active one — it names both and tells you to switch first — so a work edit can't land under personal credentials. A file with no marker (a legacy file, or one you created before configuring accounts) lets read/refresh commands through and stamps ownership on the next refresh, but refuses a mutation until then. + +Leave =pearl-accounts= unset for single-account use; everything works off =pearl-api-key= and =pearl-org-file-path= exactly as before. + +*** Saved queries across accounts + +=pearl-saved-queries= is one shared list. A query entry may carry an optional =:account= so it only runs under that account: + +#+begin_src elisp +("My work bugs" :account "work" + :filter (:team "ENG" :label "bug" :assignee :me)) +#+end_src + +Running it under a different active account refuses before any lookup or fetch. A query *without* =:account= is shared and resolves its team / state / label names against whatever account is active — so the same name can mean different things across workspaces. Tag the ones that must not cross. + ** Development & Testing :PROPERTIES: :CUSTOM_ID: development--testing |
