From f26950fa4e3ad4ade2973d70c63ce69a983ac0ce Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 11 Jun 2026 19:30:19 +0300 Subject: [PATCH] fix: treat gh CLI token as GitHub account --- packages/ui/src/components/layout/Header.tsx | 11 ++- .../sections/openchamber/GitHubSettings.tsx | 42 +++++---- packages/ui/src/lib/api/types.ts | 1 + .../ui/src/lib/i18n/messages/en.settings.ts | 2 + packages/ui/src/lib/i18n/messages/en.ts | 2 + .../ui/src/lib/i18n/messages/es.settings.ts | 2 + packages/ui/src/lib/i18n/messages/es.ts | 2 + .../ui/src/lib/i18n/messages/fr.settings.ts | 2 + packages/ui/src/lib/i18n/messages/fr.ts | 2 + .../ui/src/lib/i18n/messages/ko.settings.ts | 2 + packages/ui/src/lib/i18n/messages/ko.ts | 2 + .../ui/src/lib/i18n/messages/pl.settings.ts | 2 + packages/ui/src/lib/i18n/messages/pl.ts | 2 + .../src/lib/i18n/messages/pt-BR.settings.ts | 2 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 2 + .../ui/src/lib/i18n/messages/uk.settings.ts | 2 + packages/ui/src/lib/i18n/messages/uk.ts | 2 + .../src/lib/i18n/messages/zh-CN.settings.ts | 2 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 2 + .../src/lib/i18n/messages/zh-TW.settings.ts | 2 + packages/ui/src/lib/i18n/messages/zh-TW.ts | 2 + packages/web/server/lib/github/auth.js | 76 +++++++++------- packages/web/server/lib/github/index.js | 3 + packages/web/server/lib/github/octokit.js | 5 +- packages/web/server/lib/github/routes.js | 87 ++++++++++++++++--- 25 files changed, 196 insertions(+), 65 deletions(-) diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index af0b0f3a..f96b0b99 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -187,11 +187,14 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({ {githubAccounts.map((account) => { const accountUser = account.user; const isCurrent = Boolean(account.current); + const sourceLabel = account.source === 'gh-cli' + ? t('header.github.accountSource.cli') + : t('header.github.accountSource.oauth'); return ( { if (!isCurrent) { void handleGitHubAccountSwitch(account.id); @@ -216,8 +219,10 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({ {accountUser?.name?.trim() || accountUser?.login || 'GitHub'} {accountUser?.login ? ( - - {accountUser.login} + + {accountUser.login} + · + {sourceLabel} ) : null} diff --git a/packages/ui/src/components/sections/openchamber/GitHubSettings.tsx b/packages/ui/src/components/sections/openchamber/GitHubSettings.tsx index 37164f16..5e9aa6a6 100644 --- a/packages/ui/src/components/sections/openchamber/GitHubSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/GitHubSettings.tsx @@ -263,7 +263,11 @@ export const GitHubSettings: React.FC = () => { const connected = Boolean(status?.connected); const user = status?.user; const accounts = status?.accounts ?? []; + const otherAccounts = accounts.filter((account) => !account.current); const ghCli = status?.ghCli ?? null; + const activeAccountSourceLabel = ghCli?.active + ? t('settings.github.page.accountSource.cli') + : t('settings.github.page.accountSource.oauth'); return (
@@ -306,6 +310,8 @@ export const GitHubSettings: React.FC = () => { {user?.login || t('settings.github.page.label.unknownUser')} {user?.email && } {user?.email && {user.email}} + + {activeAccountSourceLabel}
{status?.scope && (
@@ -341,15 +347,17 @@ export const GitHubSettings: React.FC = () => {
)} - {accounts.length > 1 && ( + {otherAccounts.length > 0 && (
{t('settings.github.page.label.otherAccounts')}
- {accounts.map((account) => { + {otherAccounts.map((account) => { const accountUser = account.user; - const isCurrent = Boolean(account.current); + const sourceLabel = account.source === 'gh-cli' + ? t('settings.github.page.accountSource.cli') + : t('settings.github.page.accountSource.oauth'); return (
{ {accountUser?.name?.trim() || accountUser?.login || 'GitHub'} {accountUser?.login && ( - - {accountUser.login} + + {accountUser.login} + · + {sourceLabel} )}
- {isCurrent ? ( - - {t('settings.github.page.status.active')} - - ) : ( - - )} +
); })} @@ -449,7 +453,7 @@ export const GitHubSettings: React.FC = () => { )} - {ghCli?.available && !ghCli?.active && ( + {ghCli?.available && !ghCli?.active && (!ghCli.user || ghCli.disabled) && (

{t('settings.github.page.ghCli.title')} diff --git a/packages/ui/src/lib/api/types.ts b/packages/ui/src/lib/api/types.ts index 3e95dcc6..c2c13623 100644 --- a/packages/ui/src/lib/api/types.ts +++ b/packages/ui/src/lib/api/types.ts @@ -1052,6 +1052,7 @@ export type GitHubAuthAccount = { user: GitHubUserSummary; scope?: string; current?: boolean; + source?: 'oauth' | 'gh-cli'; }; export type GitHubDeviceFlowStart = { diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 40eb398e..5eaaebb3 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1390,6 +1390,8 @@ export const settingsDict = { 'settings.github.page.label.unknownUser': 'unknown', 'settings.github.page.label.scopes': 'Scopes: {value}', 'settings.github.page.label.otherAccounts': 'Other Accounts', + 'settings.github.page.accountSource.oauth': 'OAuth', + 'settings.github.page.accountSource.cli': 'CLI', 'settings.github.page.actions.disconnect': 'Disconnect', 'settings.github.page.actions.connect': 'Connect GitHub', 'settings.github.page.actions.switchTo': 'Switch to', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index f7cef4c9..6e7e0c5b 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -1273,6 +1273,8 @@ export const dict = { 'header.github.avatarWithLogin': '{login} avatar', 'header.github.avatar': 'GitHub avatar', 'header.github.accountsTitle': 'GitHub Accounts', + 'header.github.accountSource.oauth': 'OAuth', + 'header.github.accountSource.cli': 'CLI', 'header.services.openWithCurrent': 'Open instance, usage and MCP (current: {current})', 'header.services.open': 'Open services, usage and MCP', 'header.services.tooltip.currentInstanceWithShortcuts': 'Current instance: {current} ({toggle}; next tab {nextTab})', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 745e2593..d52f0c24 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1357,6 +1357,8 @@ export const settingsDict = { "settings.github.page.label.unknownUser": "desconocido", "settings.github.page.label.scopes": "Alcances: {value}", "settings.github.page.label.otherAccounts": "Cuentas adicionales", + "settings.github.page.accountSource.oauth": "OAuth", + "settings.github.page.accountSource.cli": "CLI", "settings.github.page.actions.disconnect": "Desconectar", "settings.github.page.actions.connect": "Conectar GitHub", "settings.github.page.actions.switchTo": "Cambiar a", diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index f8e50da8..03a42d09 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1239,6 +1239,8 @@ export const dict: Record = { "header.github.avatarWithLogin": "Avatar de {login}", "header.github.avatar": "Avatar de GitHub", "header.github.accountsTitle": "Cuentas de GitHub", + "header.github.accountSource.oauth": "OAuth", + "header.github.accountSource.cli": "CLI", "header.services.openWithCurrent": "Abrir instancia, uso y MCP (actual: {current})", "header.services.open": "Abrir servicios, uso y MCP", "header.services.tooltip.currentInstanceWithShortcuts": "Instancia actual: {current} ({toggle}; siguiente pestaña {nextTab})", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index f89c43e3..7fc9bbf1 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -1346,6 +1346,8 @@ export const settingsDict = { 'settings.github.page.label.unknownUser': 'inconnu', 'settings.github.page.label.scopes': 'Portées : {value}', 'settings.github.page.label.otherAccounts': 'Autres comptes', + 'settings.github.page.accountSource.oauth': 'OAuth', + 'settings.github.page.accountSource.cli': 'CLI', 'settings.github.page.actions.disconnect': 'Déconnecter', 'settings.github.page.actions.connect': 'Connectez GitHub', 'settings.github.page.actions.switchTo': 'Passer à', diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 4f78dd22..356bd414 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -1145,6 +1145,8 @@ export const dict = { 'header.github.avatarWithLogin': 'Avatar {login}', 'header.github.avatar': 'Avatar GitHub', 'header.github.accountsTitle': 'Comptes GitHub', + 'header.github.accountSource.oauth': 'OAuth', + 'header.github.accountSource.cli': 'CLI', 'header.services.openWithCurrent': 'Instance ouverte, utilisation et MCP (actuel : {current})', 'header.services.open': 'Services ouverts, utilisation et MCP', 'header.services.tooltip.currentInstanceWithShortcuts': 'Instance actuelle : {current} ({toggle} ; onglet suivant {nextTab})', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index a46c8e5c..cc4c3f5d 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1357,6 +1357,8 @@ export const settingsDict = { 'settings.github.page.label.unknownUser': '알 수 없음', 'settings.github.page.label.scopes': 'Scopes: {value}', 'settings.github.page.label.otherAccounts': '다른 계정', + 'settings.github.page.accountSource.oauth': 'OAuth', + 'settings.github.page.accountSource.cli': 'CLI', 'settings.github.page.actions.disconnect': '연결 해제', 'settings.github.page.actions.connect': 'GitHub 연결', 'settings.github.page.actions.switchTo': '전환', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 4d12b9e9..fc4b7300 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -1276,6 +1276,8 @@ export const dict: Record = { 'header.github.avatarWithLogin': '{login} 아바타', 'header.github.avatar': 'GitHub 아바타', 'header.github.accountsTitle': 'GitHub 계정', + 'header.github.accountSource.oauth': 'OAuth', + 'header.github.accountSource.cli': 'CLI', 'header.services.openWithCurrent': '인스턴스, 사용량, MCP 열기(현재: {current})', 'header.services.open': '서비스, 사용량, MCP 열기', 'header.services.tooltip.currentInstanceWithShortcuts': '현재 인스턴스: {current} ({toggle}; 다음 탭 {nextTab})', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 6ada90f1..3b5db2f9 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -235,6 +235,8 @@ export const settingsDict = { 'settings.github.page.flow.title': 'Autoryzuj OpenChamber', 'settings.github.page.flow.waiting': 'Oczekiwanie na zatwierdzenie... (automatyczne odświeżanie)', 'settings.github.page.label.otherAccounts': 'Inne konta', + 'settings.github.page.accountSource.oauth': 'OAuth', + 'settings.github.page.accountSource.cli': 'CLI', 'settings.github.page.label.scopes': 'Zakresy: {value}', 'settings.github.page.label.unknownUser': 'nieznany', 'settings.github.page.status.active': 'Aktywne', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 6c6dad90..1fb9f0aa 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1960,6 +1960,8 @@ export const dict: Record = { 'header.actions.toggleTerminalPanelAria': 'Przełącz panel terminala', 'header.changes.availableAria': 'Dostępne zmiany', 'header.github.accountsTitle': 'Konta GitHub', + 'header.github.accountSource.oauth': 'OAuth', + 'header.github.accountSource.cli': 'CLI', 'header.github.avatar': 'Awatar GitHub', 'header.github.avatarWithLogin': 'Awatar {login}', 'header.github.connected': 'GitHub połączony', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index c27874c7..c524c24c 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1357,6 +1357,8 @@ export const settingsDict = { "settings.github.page.label.unknownUser": "desconhecido", "settings.github.page.label.scopes": "Alcances: {value}", "settings.github.page.label.otherAccounts": "Contas adicionales", + "settings.github.page.accountSource.oauth": "OAuth", + "settings.github.page.accountSource.cli": "CLI", "settings.github.page.actions.disconnect": "Desconectar", "settings.github.page.actions.connect": "Conectar GitHub", "settings.github.page.actions.switchTo": "Alternar para", diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 9c5390de..f653a34b 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1239,6 +1239,8 @@ export const dict: Record = { "header.github.avatarWithLogin": "Avatar de {login}", "header.github.avatar": "Avatar de GitHub", "header.github.accountsTitle": "Contas de GitHub", + "header.github.accountSource.oauth": "OAuth", + "header.github.accountSource.cli": "CLI", "header.services.openWithCurrent": "Abrir instância, uso e MCP (atual: {current})", "header.services.open": "Abrir serviços, uso e MCP", "header.services.tooltip.currentInstanceWithShortcuts": "Instância atual: {current} ({toggle}; próxima aba {nextTab})", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 0399e985..c29c728f 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1357,6 +1357,8 @@ export const settingsDict = { "settings.github.page.label.unknownUser": "невідомий", "settings.github.page.label.scopes": "Області застосування: {value}", "settings.github.page.label.otherAccounts": "Інші облікові записи", + "settings.github.page.accountSource.oauth": "OAuth", + "settings.github.page.accountSource.cli": "CLI", "settings.github.page.actions.disconnect": "Відключити", "settings.github.page.actions.connect": "Підключити GitHub", "settings.github.page.actions.switchTo": "Перемкнутися на", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 45b6c39b..1a0cbe50 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1239,6 +1239,8 @@ export const dict: Record = { "header.github.avatarWithLogin": "Аватар {login}", "header.github.avatar": "Аватар GitHub", "header.github.accountsTitle": "Облікові записи GitHub", + "header.github.accountSource.oauth": "OAuth", + "header.github.accountSource.cli": "CLI", "header.services.openWithCurrent": "Відкрити інстанс, використання та MCP (поточний: {current})", "header.services.open": "Відкрити сервіси, використання та MCP", "header.services.tooltip.currentInstanceWithShortcuts": "Поточний інстанс: {current} ({toggle}; наступна вкладка {nextTab})", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 0d203aa5..44f57c5a 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1357,6 +1357,8 @@ export const settingsDict = { 'settings.github.page.label.unknownUser': '未知', 'settings.github.page.label.scopes': '作用域:{value}', 'settings.github.page.label.otherAccounts': '其他账号', + 'settings.github.page.accountSource.oauth': 'OAuth', + 'settings.github.page.accountSource.cli': 'CLI', 'settings.github.page.actions.disconnect': '断开连接', 'settings.github.page.actions.connect': '连接 GitHub', 'settings.github.page.actions.switchTo': '切换到', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index fa40b361..fb4633f8 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1239,6 +1239,8 @@ export const dict: Record = { 'header.github.avatarWithLogin': '{login} 头像', 'header.github.avatar': 'GitHub 头像', 'header.github.accountsTitle': 'GitHub 账户', + 'header.github.accountSource.oauth': 'OAuth', + 'header.github.accountSource.cli': 'CLI', 'header.services.openWithCurrent': '打开实例、用量和 MCP(当前:{current})', 'header.services.open': '打开服务、用量和 MCP', 'header.services.tooltip.currentInstanceWithShortcuts': '当前实例:{current}({toggle};下一标签 {nextTab})', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 89ade3e0..25bd8400 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1278,6 +1278,8 @@ 'settings.github.page.label.unknownUser': '未知', 'settings.github.page.label.scopes': '作用域:{value}', 'settings.github.page.label.otherAccounts': '其他帳號', + 'settings.github.page.accountSource.oauth': 'OAuth', + 'settings.github.page.accountSource.cli': 'CLI', 'settings.github.page.actions.disconnect': '中斷連線', 'settings.github.page.actions.connect': '連線 GitHub', 'settings.github.page.actions.switchTo': '切換到', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index c09a0a86..f954f99d 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -1249,6 +1249,8 @@ export const dict: Record = { 'header.github.avatarWithLogin': '{login} 頭像', 'header.github.avatar': 'GitHub 頭像', 'header.github.accountsTitle': 'GitHub 帳戶', + 'header.github.accountSource.oauth': 'OAuth', + 'header.github.accountSource.cli': 'CLI', 'header.services.openWithCurrent': '開啟實例、用量和 MCP(目前:{current})', 'header.services.open': '開啟服務、用量和 MCP', 'header.services.tooltip.currentInstanceWithShortcuts': '目前實例:{current}({toggle};下一分頁 {nextTab})', diff --git a/packages/web/server/lib/github/auth.js b/packages/web/server/lib/github/auth.js index 9e0eeb83..873a2452 100644 --- a/packages/web/server/lib/github/auth.js +++ b/packages/web/server/lib/github/auth.js @@ -12,6 +12,7 @@ const SETTINGS_FILE = path.join(OPENCHAMBER_DATA_DIR, 'settings.json'); const DEFAULT_GITHUB_CLIENT_ID = 'Ov23lizomPOC3eFYo56r'; const DEFAULT_GITHUB_SCOPES = 'repo read:org workflow read:user user:email'; +export const GH_CLI_ACCOUNT_ID = 'gh-cli'; function ensureStorageDir() { if (!fs.existsSync(STORAGE_DIR)) { @@ -159,6 +160,34 @@ function writeAuthList(list) { writeJsonFile(list); } +function readSettingsFile() { + try { + if (fs.existsSync(SETTINGS_FILE)) { + return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')) || {}; + } + } catch { + // ignore + } + return {}; +} + +function writeSettingsFile(settings) { + ensureStorageDir(); + const tmpFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`; + fs.writeFileSync(tmpFile, JSON.stringify(settings, null, 2), 'utf8'); + try { + fs.chmodSync(tmpFile, 0o600); + } catch { + // best-effort + } + fs.renameSync(tmpFile, SETTINGS_FILE); + try { + fs.chmodSync(SETTINGS_FILE, 0o600); + } catch { + // best-effort + } +} + export function getGitHubAuth() { const list = readAuthList(); if (!list.length) { @@ -237,6 +266,7 @@ export function activateGitHubAuth(accountId) { if (index === -1) { return false; } + setGhCliActive(false); list.forEach((entry, idx) => { entry.current = idx === index; }); @@ -307,39 +337,25 @@ export function getGitHubScopes() { export const GITHUB_AUTH_FILE = STORAGE_FILE; export function isGhCliDisabled() { - try { - if (fs.existsSync(SETTINGS_FILE)) { - const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); - return Boolean(parsed?.ghCliDisabled); - } - } catch { - // ignore - } - return false; + return Boolean(readSettingsFile()?.ghCliDisabled); } export function setGhCliDisabled(disabled) { - ensureStorageDir(); - let settings = {}; - try { - if (fs.existsSync(SETTINGS_FILE)) { - settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')) || {}; - } - } catch { - // ignore - } + const settings = readSettingsFile(); settings.ghCliDisabled = Boolean(disabled); - const tmpFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`; - fs.writeFileSync(tmpFile, JSON.stringify(settings, null, 2), 'utf8'); - try { - fs.chmodSync(tmpFile, 0o600); - } catch { - // best-effort - } - fs.renameSync(tmpFile, SETTINGS_FILE); - try { - fs.chmodSync(SETTINGS_FILE, 0o600); - } catch { - // best-effort + if (settings.ghCliDisabled) { + settings.ghCliActive = false; } + writeSettingsFile(settings); +} + +export function isGhCliActive() { + const settings = readSettingsFile(); + return !settings?.ghCliDisabled && Boolean(settings?.ghCliActive); +} + +export function setGhCliActive(active) { + const settings = readSettingsFile(); + settings.ghCliActive = Boolean(active) && !settings.ghCliDisabled; + writeSettingsFile(settings); } diff --git a/packages/web/server/lib/github/index.js b/packages/web/server/lib/github/index.js index 075748b6..926584b7 100644 --- a/packages/web/server/lib/github/index.js +++ b/packages/web/server/lib/github/index.js @@ -6,7 +6,10 @@ export { clearGitHubAuth, getGitHubClientId, getGitHubScopes, + GH_CLI_ACCOUNT_ID, isGhCliDisabled, + isGhCliActive, + setGhCliActive, setGhCliDisabled, GITHUB_AUTH_FILE, } from './auth.js'; diff --git a/packages/web/server/lib/github/octokit.js b/packages/web/server/lib/github/octokit.js index 5ee1e2e7..b6cd15cc 100644 --- a/packages/web/server/lib/github/octokit.js +++ b/packages/web/server/lib/github/octokit.js @@ -1,10 +1,11 @@ import { Octokit } from '@octokit/rest'; -import { getGitHubAuth, isGhCliDisabled } from './auth.js'; +import { getGitHubAuth, isGhCliActive, isGhCliDisabled } from './auth.js'; import { getGhCliToken } from './gh-cli-credential.js'; export function getOctokitOrNull() { const auth = getGitHubAuth(); - const token = auth?.accessToken || (!isGhCliDisabled() ? getGhCliToken() : null); + const ghToken = !isGhCliDisabled() ? getGhCliToken() : null; + const token = isGhCliActive() ? ghToken || auth?.accessToken : auth?.accessToken || ghToken; if (!token) { return null; } diff --git a/packages/web/server/lib/github/routes.js b/packages/web/server/lib/github/routes.js index 93fa4ebf..5496f2ba 100644 --- a/packages/web/server/lib/github/routes.js +++ b/packages/web/server/lib/github/routes.js @@ -76,17 +76,18 @@ export function registerGitHubRoutes(app) { app.get('/api/github/auth/status', async (_req, res) => { try { - const { getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, isGhCliDisabled } = await getGitHubLibraries(); + const { getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, GH_CLI_ACCOUNT_ID, isGhCliActive, isGhCliDisabled, setGhCliActive } = await getGitHubLibraries(); const { getGhCliToken } = await import('./gh-cli-credential.js'); const auth = getGitHubAuth(); - const accounts = getGitHubAuthAccounts(); + let accounts = getGitHubAuthAccounts(); const ghCliDisabled = isGhCliDisabled(); + const ghCliActive = isGhCliActive(); const ghToken = getGhCliToken(); const usingOwnToken = Boolean(auth?.accessToken); let ghCliUser = null; - if (ghToken !== null && !ghCliDisabled && usingOwnToken) { + if (ghToken !== null && !ghCliDisabled) { try { const { Octokit } = await import('@octokit/rest'); ghCliUser = await getGitHubUserSummary(new Octokit({ auth: ghToken })); @@ -94,11 +95,26 @@ export function registerGitHubRoutes(app) { ghCliUser = null; } } + if (ghCliActive && !ghCliUser) { + setGhCliActive(false); + } + + const ghCliCurrent = ghToken !== null && !ghCliDisabled && Boolean(ghCliUser) && (ghCliActive || !usingOwnToken); + if (ghCliUser) { + accounts = accounts + .map((account) => ({ ...account, current: ghCliCurrent ? false : Boolean(account.current) })) + .concat({ + id: GH_CLI_ACCOUNT_ID, + user: ghCliUser, + current: ghCliCurrent, + source: 'gh-cli', + }); + } const buildGhCli = (activeUser = null) => ({ available: ghToken !== null, disabled: ghCliDisabled, - active: !usingOwnToken && ghToken !== null && !ghCliDisabled, + active: ghCliCurrent, ...(!ghCliDisabled && (activeUser || ghCliUser) ? { user: activeUser || ghCliUser } : {}), }); @@ -119,14 +135,12 @@ export function registerGitHubRoutes(app) { const fallback = usingOwnToken ? auth.user : null; const mergedUser = user || fallback; - const ghIsActive = !usingOwnToken && ghToken !== null && !ghCliDisabled; - return res.json({ connected: true, user: mergedUser, - scope: usingOwnToken ? auth.scope : undefined, + scope: ghCliCurrent ? undefined : auth?.scope, accounts, - ghCli: buildGhCli(ghIsActive ? mergedUser : null), + ghCli: buildGhCli(ghCliCurrent ? mergedUser : null), }); } catch (error) { console.error('Failed to get GitHub auth status:', error); @@ -238,25 +252,70 @@ export function registerGitHubRoutes(app) { app.post('/api/github/auth/activate', async (req, res) => { try { - const { activateGitHubAuth, getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts } = await getGitHubLibraries(); + const { activateGitHubAuth, getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, GH_CLI_ACCOUNT_ID, isGhCliDisabled, setGhCliActive } = await getGitHubLibraries(); const accountId = typeof req.body?.accountId === 'string' ? req.body.accountId : ''; if (!accountId) { return res.status(400).json({ error: 'accountId is required' }); } + if (accountId === GH_CLI_ACCOUNT_ID) { + const { getGhCliToken } = await import('./gh-cli-credential.js'); + const ghToken = !isGhCliDisabled() ? getGhCliToken() : null; + if (!ghToken) { + return res.status(404).json({ error: 'GitHub CLI account not found' }); + } + + const { Octokit } = await import('@octokit/rest'); + const user = await getGitHubUserSummary(new Octokit({ auth: ghToken })); + setGhCliActive(true); + const accounts = getGitHubAuthAccounts() + .map((account) => ({ ...account, current: false })) + .concat({ id: GH_CLI_ACCOUNT_ID, user, current: true, source: 'gh-cli' }); + return res.json({ + connected: true, + user, + accounts, + ghCli: { + available: true, + disabled: false, + active: true, + user, + }, + }); + } + const activated = activateGitHubAuth(accountId); if (!activated) { return res.status(404).json({ error: 'GitHub account not found' }); } const auth = getGitHubAuth(); - const accounts = getGitHubAuthAccounts(); + let accounts = getGitHubAuthAccounts(); if (!auth?.accessToken) { return res.json({ connected: false, accounts }); } + const { getGhCliToken } = await import('./gh-cli-credential.js'); + const ghCliDisabled = isGhCliDisabled(); + const ghToken = !ghCliDisabled ? getGhCliToken() : null; + let ghCliUser = null; + if (ghToken) { + try { + const { Octokit } = await import('@octokit/rest'); + ghCliUser = await getGitHubUserSummary(new Octokit({ auth: ghToken })); + accounts = accounts.concat({ + id: GH_CLI_ACCOUNT_ID, + user: ghCliUser, + current: false, + source: 'gh-cli', + }); + } catch { + ghCliUser = null; + } + } + const octokit = getOctokitOrNull(); if (!octokit) { - return res.json({ connected: false, accounts }); + return res.json({ connected: false, accounts, ghCli: { available: ghToken !== null, disabled: ghCliDisabled, active: false, ...(ghCliUser ? { user: ghCliUser } : {}) } }); } let user = auth.user || null; @@ -274,6 +333,12 @@ export function registerGitHubRoutes(app) { user, scope: auth.scope, accounts, + ghCli: { + available: ghToken !== null, + disabled: ghCliDisabled, + active: false, + ...(ghCliUser ? { user: ghCliUser } : {}), + }, }); } catch (error) { console.error('Failed to activate GitHub account:', error);