feat: multi-account support for github.com#158
Open
dokun1 wants to merge 43 commits intogharlan:mainfrom
Open
feat: multi-account support for github.com#158dokun1 wants to merge 43 commits intogharlan:mainfrom
dokun1 wants to merge 43 commits intogharlan:mainfrom
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ser open on switch
…sing tests, fix command injection
There was a problem hiding this comment.
Pull request overview
Adds multi-account support for github.com by introducing an accounts table, migrating legacy single-token storage, and partitioning the request cache per active account (while keeping GitHub Enterprise on the legacy token path). The PR also adds a PHPUnit harness + CI coverage to prevent regressions across the new migration and routing behavior.
Changes:
- Introduce
accountsstorage + active-account switching, and migrate legacyaccess_tokenon init. - Partition
request_cachebyaccount_id(enterprise uses sentinel0) and migrate legacy cache schema. - Add
> user ...commands (add/login/switch/update/delete), update OAuth callback handling, and add PHPUnit tests + GitHub Actions CI.
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
workflow.php |
Adds accounts CRUD + legacy migrations; partitions request cache by account. |
action.php |
Refactors into Action::dispatch(), adds user subcommand handlers, and hardens open exec quoting. |
search.php |
Adds > user ... command rendering and shows active account in the gh header. |
server.php |
OAuth callback now stores tokens under a label and can resolve username via /user. |
composer.json |
Adds PHPUnit dev dependency and composer test script. |
phpunit.xml.dist |
PHPUnit configuration and bootstrap wiring. |
.github/workflows/test.yml |
CI matrix running PHPUnit on PHP 8.2–8.4. |
.gitignore |
Ignores PHPUnit cache and local composer artifacts. |
tests/bootstrap.php |
Test bootstrap for loading workflow files and resetting static state. |
tests/WorkflowTestCase.php |
Base test case for isolated temp data dirs + Workflow static reset. |
tests/WorkflowTokenTest.php |
Tests token isolation and multi-account token behaviors. |
tests/WorkflowSchemaTest.php |
Pins schema shape and init idempotency. |
tests/WorkflowEnterpriseUrlTest.php |
Pins enterprise URL derivation behavior. |
tests/WorkflowConfigTest.php |
Pins config CRUD behavior. |
tests/SearchHeaderTest.php |
Verifies header includes active account label. |
tests/RequestCachePartitionTest.php |
Verifies cache partitioning + legacy migration. |
tests/ItemRenderTest.php |
Characterization tests for Item::toXml. |
tests/CurlRequestTest.php |
Pins CurlRequest value-object behavior. |
tests/ActionDispatchTest.php |
Covers command routing including new user subcommands. |
tests/AccountsSchemaTest.php |
Verifies accounts table constraints and index. |
tests/AccountsMigrationTest.php |
Verifies legacy token migration behavior. |
tests/AccountsCrudTest.php |
Exercises accounts CRUD + deleteDatabase preservation behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+61
to
+65
| self::ensureAccountsTable(); | ||
| self::migrateRequestCacheSchema(); | ||
|
|
||
| if (!self::$enterprise) { | ||
| self::migrateLegacyAccessToken(); |
| PRIMARY KEY (account_id, url) | ||
| ) WITHOUT ROWID | ||
| '); | ||
| self::$db->exec('CREATE INDEX parent_url ON request_cache(parent) WHERE parent IS NOT NULL'); |
| $stmt = self::$db->prepare( | ||
| 'INSERT INTO accounts (label, token, is_active, created_at) VALUES (?, ?, 1, ?)' | ||
| ); | ||
| $stmt->execute(['default', $legacyToken, time()]); |
Comment on lines
+119
to
+128
| private static function showAlert(string $title, string $message): void | ||
| { | ||
| $safeTitle = str_replace('"', '\\"', $title); | ||
| $safeMessage = str_replace('"', '\\"', $message); | ||
| $script = 'set r to display dialog "'.$safeMessage.'" buttons {"OK", "Open GitHub"} default button "Open GitHub" with title "'.$safeTitle.'"'."\n". | ||
| 'if button returned of r is "Open GitHub" then'."\n". | ||
| ' open location "https://github.com"'."\n". | ||
| 'end if'; | ||
| exec('osascript -e '.escapeshellarg($script).' > /dev/null 2>&1 &'); | ||
| } |
Comment on lines
+137
to
+142
| if ($enterprise) { | ||
| return 'Multi-account is only supported for github.com.'; | ||
| } | ||
| if ('' === $label) { | ||
| return 'Usage: gh user add <label>'; | ||
| } |
Comment on lines
+30
to
+31
| unset($ch); | ||
| if (200 === $status) { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds multi-account support for github.com — users can register multiple GitHub accounts and switch between them.
Important
This PR depends on #157 (
tests/add-phpunit-harness). Please merge that first — this branch is stacked on top of it. Once #157 is merged, I'll rebase this PR ontomainand update the base branch.What it does
gh > user add <label>— starts the OAuth flow withlogin=<label>&prompt=select_accountso GitHub shows the account picker. The label is stored locally;server.phpsaves the token under that label when the callback completes. If no explicit label is pending (legacygh > login), the callback resolves the actual GitHub username via the/userAPI.gh > user switch— lists all registered accounts; selecting one switches the active account and shows a macOS alert dialog with an "Open GitHub" button to sync the browser session.gh > user switch <label>— direct switch by name.gh > user update <label>— re-runs OAuth to refresh a token.gh > user delete <label>— shows a confirmation dialog, then removes the account and its cached data. Errors if the account is currently active.gh > user login <label> <token>— manual PAT fallback for cases where OAuth doesn't work.What stays the same
gh my pulls,gh > login,gh > logout,gh > delete cache,ghe > url,ghe > login, etc.) work identically.ghe >commands use the same config-based token path as before.access_tokenis automatically migrated to an account labeled with the user's GitHub username. Everything keeps working.Behavioral changes to note
gh > delete databasenow clearsrequest_cacheandconfigtables but preserves the accounts table, so users don't have to re-authorize every account on a cache reset. Previously it deleted the entire SQLite file.gh > logoutclears the active account's token but preserves the account row, so re-login keeps the label.gh/gheprompt shown when the Alfred bar is empty) now showsgh (<username>)to indicate which account is active.exec('open '.$query)inaction.phpnow usesescapeshellarg()to prevent command injection from crafted URLs.Architecture
Schema
A new
accountstable holds credentials:request_cachegains anaccount_idcolumn with a rebuilt composite primary key(account_id, url)so cached API responses are partitioned per account. Enterprise cache uses sentinelaccount_id = 0.Upgrade path
On first
init()after upgrade:ensureAccountsTable()creates the accounts table idempotently (CREATE TABLE IF NOT EXISTS).migrateRequestCacheSchema()detects the missingaccount_idcolumn viaPRAGMA table_infoand rebuilds the table inside a transaction.migrateLegacyAccessToken()seeds the existingaccess_tokenconfig value into an active account row.All three steps are idempotent and safe to run on every invocation.
Files changed (production)
workflow.phpaction.phpAction::dispatch()for testability; addedusersubcommand routing; macOS alert dialogssearch.php> user switch/delete/update/add/loginrender account listsserver.phpFiles unchanged
curl.php,item.php,info.plist,.php-cs-fixer.dist.php— zero modifications.Test plan
110 automated tests, 208 assertions (includes the 30 characterization tests from PR #157):
AccountsSchemaTestAccountsMigrationTestRequestCachePartitionTestAccountsCrudTestActionDispatchTestusersubcommand branches (add/login/switch/update/delete)SearchHeaderTestghheaderWorkflowTokenTestManual QA performed: fresh install → login (label auto-resolved to GitHub username) → add second account via OAuth with account picker → switch (alert dialog) → verify API results change → delete (confirmation dialog) → re-add → logout/re-login → delete database (accounts survive).
vendor/bin/phpunit— 110 tests, 208 assertions, all green (PHP 8.5)🤖 Generated with Claude Code