fix: treat gh CLI token as GitHub account

This commit is contained in:
Bohdan Triapitsyn
2026-06-11 19:30:19 +03:00
parent 465732858f
commit f26950fa4e
25 changed files with 196 additions and 65 deletions
+8 -3
View File
@@ -187,11 +187,14 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({
{githubAccounts.map((account) => { {githubAccounts.map((account) => {
const accountUser = account.user; const accountUser = account.user;
const isCurrent = Boolean(account.current); const isCurrent = Boolean(account.current);
const sourceLabel = account.source === 'gh-cli'
? t('header.github.accountSource.cli')
: t('header.github.accountSource.oauth');
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={account.id} key={account.id}
className="gap-2" className="gap-2"
disabled={isCurrent || isSwitchingGitHubAccount} disabled={isSwitchingGitHubAccount}
onSelect={() => { onSelect={() => {
if (!isCurrent) { if (!isCurrent) {
void handleGitHubAccountSwitch(account.id); void handleGitHubAccountSwitch(account.id);
@@ -216,8 +219,10 @@ const DesktopGitHubControl = React.memo(function DesktopGitHubControl({
{accountUser?.name?.trim() || accountUser?.login || 'GitHub'} {accountUser?.name?.trim() || accountUser?.login || 'GitHub'}
</span> </span>
{accountUser?.login ? ( {accountUser?.login ? (
<span className="truncate typography-micro font-mono text-muted-foreground"> <span className="truncate typography-micro text-muted-foreground">
{accountUser.login} <span className="font-mono">{accountUser.login}</span>
<span className="mx-1 opacity-50">·</span>
<span>{sourceLabel}</span>
</span> </span>
) : null} ) : null}
</span> </span>
@@ -263,7 +263,11 @@ export const GitHubSettings: React.FC = () => {
const connected = Boolean(status?.connected); const connected = Boolean(status?.connected);
const user = status?.user; const user = status?.user;
const accounts = status?.accounts ?? []; const accounts = status?.accounts ?? [];
const otherAccounts = accounts.filter((account) => !account.current);
const ghCli = status?.ghCli ?? null; const ghCli = status?.ghCli ?? null;
const activeAccountSourceLabel = ghCli?.active
? t('settings.github.page.accountSource.cli')
: t('settings.github.page.accountSource.oauth');
return ( return (
<div className="mb-8"> <div className="mb-8">
@@ -306,6 +310,8 @@ export const GitHubSettings: React.FC = () => {
<span className="font-mono">{user?.login || t('settings.github.page.label.unknownUser')}</span> <span className="font-mono">{user?.login || t('settings.github.page.label.unknownUser')}</span>
{user?.email && <span className="opacity-50"></span>} {user?.email && <span className="opacity-50"></span>}
{user?.email && <span>{user.email}</span>} {user?.email && <span>{user.email}</span>}
<span className="opacity-50"></span>
<span>{activeAccountSourceLabel}</span>
</div> </div>
{status?.scope && ( {status?.scope && (
<div className="typography-micro text-muted-foreground/70 mt-0.5"> <div className="typography-micro text-muted-foreground/70 mt-0.5">
@@ -341,15 +347,17 @@ export const GitHubSettings: React.FC = () => {
</div> </div>
)} )}
{accounts.length > 1 && ( {otherAccounts.length > 0 && (
<div className="mt-2 border-t border-[var(--surface-subtle)] pt-2 px-2 pb-1"> <div className="mt-2 border-t border-[var(--surface-subtle)] pt-2 px-2 pb-1">
<div className="typography-micro text-muted-foreground mb-2 px-1"> <div className="typography-micro text-muted-foreground mb-2 px-1">
{t('settings.github.page.label.otherAccounts')} {t('settings.github.page.label.otherAccounts')}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{accounts.map((account) => { {otherAccounts.map((account) => {
const accountUser = account.user; 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 ( return (
<div <div
key={account.id} key={account.id}
@@ -374,25 +382,21 @@ export const GitHubSettings: React.FC = () => {
{accountUser?.name?.trim() || accountUser?.login || 'GitHub'} {accountUser?.name?.trim() || accountUser?.login || 'GitHub'}
</span> </span>
{accountUser?.login && ( {accountUser?.login && (
<span className="typography-micro text-muted-foreground truncate font-mono"> <span className="typography-micro text-muted-foreground truncate">
{accountUser.login} <span className="font-mono">{accountUser.login}</span>
<span className="mx-1 opacity-50">·</span>
<span>{sourceLabel}</span>
</span> </span>
)} )}
</div> </div>
</div> </div>
{isCurrent ? ( <Button size="sm"
<span className="typography-micro text-[var(--primary-base)] bg-[var(--primary-base)]/10 px-1.5 py-0.5 rounded"> variant="ghost"
{t('settings.github.page.status.active')} onClick={() => activateAccount(account.id)}
</span> disabled={isBusy}
) : ( >
<Button size="sm" {t('settings.github.page.actions.switchTo')}
variant="ghost" </Button>
onClick={() => activateAccount(account.id)}
disabled={isBusy}
>
{t('settings.github.page.actions.switchTo')}
</Button>
)}
</div> </div>
); );
})} })}
@@ -449,7 +453,7 @@ export const GitHubSettings: React.FC = () => {
</div> </div>
)} )}
{ghCli?.available && !ghCli?.active && ( {ghCli?.available && !ghCli?.active && (!ghCli.user || ghCli.disabled) && (
<div className="mt-6"> <div className="mt-6">
<h3 className="typography-ui-header font-semibold text-foreground mb-3 px-1"> <h3 className="typography-ui-header font-semibold text-foreground mb-3 px-1">
{t('settings.github.page.ghCli.title')} {t('settings.github.page.ghCli.title')}
+1
View File
@@ -1052,6 +1052,7 @@ export type GitHubAuthAccount = {
user: GitHubUserSummary; user: GitHubUserSummary;
scope?: string; scope?: string;
current?: boolean; current?: boolean;
source?: 'oauth' | 'gh-cli';
}; };
export type GitHubDeviceFlowStart = { export type GitHubDeviceFlowStart = {
@@ -1390,6 +1390,8 @@ export const settingsDict = {
'settings.github.page.label.unknownUser': 'unknown', 'settings.github.page.label.unknownUser': 'unknown',
'settings.github.page.label.scopes': 'Scopes: {value}', 'settings.github.page.label.scopes': 'Scopes: {value}',
'settings.github.page.label.otherAccounts': 'Other Accounts', '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.disconnect': 'Disconnect',
'settings.github.page.actions.connect': 'Connect GitHub', 'settings.github.page.actions.connect': 'Connect GitHub',
'settings.github.page.actions.switchTo': 'Switch to', 'settings.github.page.actions.switchTo': 'Switch to',
+2
View File
@@ -1273,6 +1273,8 @@ export const dict = {
'header.github.avatarWithLogin': '{login} avatar', 'header.github.avatarWithLogin': '{login} avatar',
'header.github.avatar': 'GitHub avatar', 'header.github.avatar': 'GitHub avatar',
'header.github.accountsTitle': 'GitHub Accounts', '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.openWithCurrent': 'Open instance, usage and MCP (current: {current})',
'header.services.open': 'Open services, usage and MCP', 'header.services.open': 'Open services, usage and MCP',
'header.services.tooltip.currentInstanceWithShortcuts': 'Current instance: {current} ({toggle}; next tab {nextTab})', 'header.services.tooltip.currentInstanceWithShortcuts': 'Current instance: {current} ({toggle}; next tab {nextTab})',
@@ -1357,6 +1357,8 @@ export const settingsDict = {
"settings.github.page.label.unknownUser": "desconocido", "settings.github.page.label.unknownUser": "desconocido",
"settings.github.page.label.scopes": "Alcances: {value}", "settings.github.page.label.scopes": "Alcances: {value}",
"settings.github.page.label.otherAccounts": "Cuentas adicionales", "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.disconnect": "Desconectar",
"settings.github.page.actions.connect": "Conectar GitHub", "settings.github.page.actions.connect": "Conectar GitHub",
"settings.github.page.actions.switchTo": "Cambiar a", "settings.github.page.actions.switchTo": "Cambiar a",
+2
View File
@@ -1239,6 +1239,8 @@ export const dict: Record<I18nKey, string> = {
"header.github.avatarWithLogin": "Avatar de {login}", "header.github.avatarWithLogin": "Avatar de {login}",
"header.github.avatar": "Avatar de GitHub", "header.github.avatar": "Avatar de GitHub",
"header.github.accountsTitle": "Cuentas 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.openWithCurrent": "Abrir instancia, uso y MCP (actual: {current})",
"header.services.open": "Abrir servicios, uso y MCP", "header.services.open": "Abrir servicios, uso y MCP",
"header.services.tooltip.currentInstanceWithShortcuts": "Instancia actual: {current} ({toggle}; siguiente pestaña {nextTab})", "header.services.tooltip.currentInstanceWithShortcuts": "Instancia actual: {current} ({toggle}; siguiente pestaña {nextTab})",
@@ -1346,6 +1346,8 @@ export const settingsDict = {
'settings.github.page.label.unknownUser': 'inconnu', 'settings.github.page.label.unknownUser': 'inconnu',
'settings.github.page.label.scopes': 'Portées : {value}', 'settings.github.page.label.scopes': 'Portées : {value}',
'settings.github.page.label.otherAccounts': 'Autres comptes', '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.disconnect': 'Déconnecter',
'settings.github.page.actions.connect': 'Connectez GitHub', 'settings.github.page.actions.connect': 'Connectez GitHub',
'settings.github.page.actions.switchTo': 'Passer à', 'settings.github.page.actions.switchTo': 'Passer à',
+2
View File
@@ -1145,6 +1145,8 @@ export const dict = {
'header.github.avatarWithLogin': 'Avatar {login}', 'header.github.avatarWithLogin': 'Avatar {login}',
'header.github.avatar': 'Avatar GitHub', 'header.github.avatar': 'Avatar GitHub',
'header.github.accountsTitle': 'Comptes 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.openWithCurrent': 'Instance ouverte, utilisation et MCP (actuel : {current})',
'header.services.open': 'Services ouverts, utilisation et MCP', 'header.services.open': 'Services ouverts, utilisation et MCP',
'header.services.tooltip.currentInstanceWithShortcuts': 'Instance actuelle : {current} ({toggle} ; onglet suivant {nextTab})', 'header.services.tooltip.currentInstanceWithShortcuts': 'Instance actuelle : {current} ({toggle} ; onglet suivant {nextTab})',
@@ -1357,6 +1357,8 @@ export const settingsDict = {
'settings.github.page.label.unknownUser': '알 수 없음', 'settings.github.page.label.unknownUser': '알 수 없음',
'settings.github.page.label.scopes': 'Scopes: {value}', 'settings.github.page.label.scopes': 'Scopes: {value}',
'settings.github.page.label.otherAccounts': '다른 계정', '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.disconnect': '연결 해제',
'settings.github.page.actions.connect': 'GitHub 연결', 'settings.github.page.actions.connect': 'GitHub 연결',
'settings.github.page.actions.switchTo': '전환', 'settings.github.page.actions.switchTo': '전환',
+2
View File
@@ -1276,6 +1276,8 @@ export const dict: Record<I18nKey, string> = {
'header.github.avatarWithLogin': '{login} 아바타', 'header.github.avatarWithLogin': '{login} 아바타',
'header.github.avatar': 'GitHub 아바타', 'header.github.avatar': 'GitHub 아바타',
'header.github.accountsTitle': 'GitHub 계정', 'header.github.accountsTitle': 'GitHub 계정',
'header.github.accountSource.oauth': 'OAuth',
'header.github.accountSource.cli': 'CLI',
'header.services.openWithCurrent': '인스턴스, 사용량, MCP 열기(현재: {current})', 'header.services.openWithCurrent': '인스턴스, 사용량, MCP 열기(현재: {current})',
'header.services.open': '서비스, 사용량, MCP 열기', 'header.services.open': '서비스, 사용량, MCP 열기',
'header.services.tooltip.currentInstanceWithShortcuts': '현재 인스턴스: {current} ({toggle}; 다음 탭 {nextTab})', 'header.services.tooltip.currentInstanceWithShortcuts': '현재 인스턴스: {current} ({toggle}; 다음 탭 {nextTab})',
@@ -235,6 +235,8 @@ export const settingsDict = {
'settings.github.page.flow.title': 'Autoryzuj OpenChamber', 'settings.github.page.flow.title': 'Autoryzuj OpenChamber',
'settings.github.page.flow.waiting': 'Oczekiwanie na zatwierdzenie... (automatyczne odświeżanie)', 'settings.github.page.flow.waiting': 'Oczekiwanie na zatwierdzenie... (automatyczne odświeżanie)',
'settings.github.page.label.otherAccounts': 'Inne konta', '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.scopes': 'Zakresy: {value}',
'settings.github.page.label.unknownUser': 'nieznany', 'settings.github.page.label.unknownUser': 'nieznany',
'settings.github.page.status.active': 'Aktywne', 'settings.github.page.status.active': 'Aktywne',
+2
View File
@@ -1960,6 +1960,8 @@ export const dict: Record<I18nKey, string> = {
'header.actions.toggleTerminalPanelAria': 'Przełącz panel terminala', 'header.actions.toggleTerminalPanelAria': 'Przełącz panel terminala',
'header.changes.availableAria': 'Dostępne zmiany', 'header.changes.availableAria': 'Dostępne zmiany',
'header.github.accountsTitle': 'Konta GitHub', 'header.github.accountsTitle': 'Konta GitHub',
'header.github.accountSource.oauth': 'OAuth',
'header.github.accountSource.cli': 'CLI',
'header.github.avatar': 'Awatar GitHub', 'header.github.avatar': 'Awatar GitHub',
'header.github.avatarWithLogin': 'Awatar {login}', 'header.github.avatarWithLogin': 'Awatar {login}',
'header.github.connected': 'GitHub połączony', 'header.github.connected': 'GitHub połączony',
@@ -1357,6 +1357,8 @@ export const settingsDict = {
"settings.github.page.label.unknownUser": "desconhecido", "settings.github.page.label.unknownUser": "desconhecido",
"settings.github.page.label.scopes": "Alcances: {value}", "settings.github.page.label.scopes": "Alcances: {value}",
"settings.github.page.label.otherAccounts": "Contas adicionales", "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.disconnect": "Desconectar",
"settings.github.page.actions.connect": "Conectar GitHub", "settings.github.page.actions.connect": "Conectar GitHub",
"settings.github.page.actions.switchTo": "Alternar para", "settings.github.page.actions.switchTo": "Alternar para",
@@ -1239,6 +1239,8 @@ export const dict: Record<I18nKey, string> = {
"header.github.avatarWithLogin": "Avatar de {login}", "header.github.avatarWithLogin": "Avatar de {login}",
"header.github.avatar": "Avatar de GitHub", "header.github.avatar": "Avatar de GitHub",
"header.github.accountsTitle": "Contas 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.openWithCurrent": "Abrir instância, uso e MCP (atual: {current})",
"header.services.open": "Abrir serviços, uso e MCP", "header.services.open": "Abrir serviços, uso e MCP",
"header.services.tooltip.currentInstanceWithShortcuts": "Instância atual: {current} ({toggle}; próxima aba {nextTab})", "header.services.tooltip.currentInstanceWithShortcuts": "Instância atual: {current} ({toggle}; próxima aba {nextTab})",
@@ -1357,6 +1357,8 @@ export const settingsDict = {
"settings.github.page.label.unknownUser": "невідомий", "settings.github.page.label.unknownUser": "невідомий",
"settings.github.page.label.scopes": "Області застосування: {value}", "settings.github.page.label.scopes": "Області застосування: {value}",
"settings.github.page.label.otherAccounts": "Інші облікові записи", "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.disconnect": "Відключити",
"settings.github.page.actions.connect": "Підключити GitHub", "settings.github.page.actions.connect": "Підключити GitHub",
"settings.github.page.actions.switchTo": "Перемкнутися на", "settings.github.page.actions.switchTo": "Перемкнутися на",
+2
View File
@@ -1239,6 +1239,8 @@ export const dict: Record<I18nKey, string> = {
"header.github.avatarWithLogin": "Аватар {login}", "header.github.avatarWithLogin": "Аватар {login}",
"header.github.avatar": "Аватар GitHub", "header.github.avatar": "Аватар GitHub",
"header.github.accountsTitle": "Облікові записи GitHub", "header.github.accountsTitle": "Облікові записи GitHub",
"header.github.accountSource.oauth": "OAuth",
"header.github.accountSource.cli": "CLI",
"header.services.openWithCurrent": "Відкрити інстанс, використання та MCP (поточний: {current})", "header.services.openWithCurrent": "Відкрити інстанс, використання та MCP (поточний: {current})",
"header.services.open": "Відкрити сервіси, використання та MCP", "header.services.open": "Відкрити сервіси, використання та MCP",
"header.services.tooltip.currentInstanceWithShortcuts": "Поточний інстанс: {current} ({toggle}; наступна вкладка {nextTab})", "header.services.tooltip.currentInstanceWithShortcuts": "Поточний інстанс: {current} ({toggle}; наступна вкладка {nextTab})",
@@ -1357,6 +1357,8 @@ export const settingsDict = {
'settings.github.page.label.unknownUser': '未知', 'settings.github.page.label.unknownUser': '未知',
'settings.github.page.label.scopes': '作用域:{value}', 'settings.github.page.label.scopes': '作用域:{value}',
'settings.github.page.label.otherAccounts': '其他账号', '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.disconnect': '断开连接',
'settings.github.page.actions.connect': '连接 GitHub', 'settings.github.page.actions.connect': '连接 GitHub',
'settings.github.page.actions.switchTo': '切换到', 'settings.github.page.actions.switchTo': '切换到',
@@ -1239,6 +1239,8 @@ export const dict: Record<I18nKey, string> = {
'header.github.avatarWithLogin': '{login} 头像', 'header.github.avatarWithLogin': '{login} 头像',
'header.github.avatar': 'GitHub 头像', 'header.github.avatar': 'GitHub 头像',
'header.github.accountsTitle': 'GitHub 账户', 'header.github.accountsTitle': 'GitHub 账户',
'header.github.accountSource.oauth': 'OAuth',
'header.github.accountSource.cli': 'CLI',
'header.services.openWithCurrent': '打开实例、用量和 MCP(当前:{current}', 'header.services.openWithCurrent': '打开实例、用量和 MCP(当前:{current}',
'header.services.open': '打开服务、用量和 MCP', 'header.services.open': '打开服务、用量和 MCP',
'header.services.tooltip.currentInstanceWithShortcuts': '当前实例:{current}{toggle};下一标签 {nextTab}', 'header.services.tooltip.currentInstanceWithShortcuts': '当前实例:{current}{toggle};下一标签 {nextTab}',
@@ -1278,6 +1278,8 @@
'settings.github.page.label.unknownUser': '未知', 'settings.github.page.label.unknownUser': '未知',
'settings.github.page.label.scopes': '作用域:{value}', 'settings.github.page.label.scopes': '作用域:{value}',
'settings.github.page.label.otherAccounts': '其他帳號', '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.disconnect': '中斷連線',
'settings.github.page.actions.connect': '連線 GitHub', 'settings.github.page.actions.connect': '連線 GitHub',
'settings.github.page.actions.switchTo': '切換到', 'settings.github.page.actions.switchTo': '切換到',
@@ -1249,6 +1249,8 @@ export const dict: Record<I18nKey, string> = {
'header.github.avatarWithLogin': '{login} 頭像', 'header.github.avatarWithLogin': '{login} 頭像',
'header.github.avatar': 'GitHub 頭像', 'header.github.avatar': 'GitHub 頭像',
'header.github.accountsTitle': 'GitHub 帳戶', 'header.github.accountsTitle': 'GitHub 帳戶',
'header.github.accountSource.oauth': 'OAuth',
'header.github.accountSource.cli': 'CLI',
'header.services.openWithCurrent': '開啟實例、用量和 MCP(目前:{current}', 'header.services.openWithCurrent': '開啟實例、用量和 MCP(目前:{current}',
'header.services.open': '開啟服務、用量和 MCP', 'header.services.open': '開啟服務、用量和 MCP',
'header.services.tooltip.currentInstanceWithShortcuts': '目前實例:{current}{toggle};下一分頁 {nextTab}', 'header.services.tooltip.currentInstanceWithShortcuts': '目前實例:{current}{toggle};下一分頁 {nextTab}',
+46 -30
View File
@@ -12,6 +12,7 @@ const SETTINGS_FILE = path.join(OPENCHAMBER_DATA_DIR, 'settings.json');
const DEFAULT_GITHUB_CLIENT_ID = 'Ov23lizomPOC3eFYo56r'; const DEFAULT_GITHUB_CLIENT_ID = 'Ov23lizomPOC3eFYo56r';
const DEFAULT_GITHUB_SCOPES = 'repo read:org workflow read:user user:email'; const DEFAULT_GITHUB_SCOPES = 'repo read:org workflow read:user user:email';
export const GH_CLI_ACCOUNT_ID = 'gh-cli';
function ensureStorageDir() { function ensureStorageDir() {
if (!fs.existsSync(STORAGE_DIR)) { if (!fs.existsSync(STORAGE_DIR)) {
@@ -159,6 +160,34 @@ function writeAuthList(list) {
writeJsonFile(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() { export function getGitHubAuth() {
const list = readAuthList(); const list = readAuthList();
if (!list.length) { if (!list.length) {
@@ -237,6 +266,7 @@ export function activateGitHubAuth(accountId) {
if (index === -1) { if (index === -1) {
return false; return false;
} }
setGhCliActive(false);
list.forEach((entry, idx) => { list.forEach((entry, idx) => {
entry.current = idx === index; entry.current = idx === index;
}); });
@@ -307,39 +337,25 @@ export function getGitHubScopes() {
export const GITHUB_AUTH_FILE = STORAGE_FILE; export const GITHUB_AUTH_FILE = STORAGE_FILE;
export function isGhCliDisabled() { export function isGhCliDisabled() {
try { return Boolean(readSettingsFile()?.ghCliDisabled);
if (fs.existsSync(SETTINGS_FILE)) {
const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
return Boolean(parsed?.ghCliDisabled);
}
} catch {
// ignore
}
return false;
} }
export function setGhCliDisabled(disabled) { export function setGhCliDisabled(disabled) {
ensureStorageDir(); const settings = readSettingsFile();
let settings = {};
try {
if (fs.existsSync(SETTINGS_FILE)) {
settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')) || {};
}
} catch {
// ignore
}
settings.ghCliDisabled = Boolean(disabled); settings.ghCliDisabled = Boolean(disabled);
const tmpFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`; if (settings.ghCliDisabled) {
fs.writeFileSync(tmpFile, JSON.stringify(settings, null, 2), 'utf8'); settings.ghCliActive = false;
try {
fs.chmodSync(tmpFile, 0o600);
} catch {
// best-effort
}
fs.renameSync(tmpFile, SETTINGS_FILE);
try {
fs.chmodSync(SETTINGS_FILE, 0o600);
} catch {
// best-effort
} }
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);
} }
+3
View File
@@ -6,7 +6,10 @@ export {
clearGitHubAuth, clearGitHubAuth,
getGitHubClientId, getGitHubClientId,
getGitHubScopes, getGitHubScopes,
GH_CLI_ACCOUNT_ID,
isGhCliDisabled, isGhCliDisabled,
isGhCliActive,
setGhCliActive,
setGhCliDisabled, setGhCliDisabled,
GITHUB_AUTH_FILE, GITHUB_AUTH_FILE,
} from './auth.js'; } from './auth.js';
+3 -2
View File
@@ -1,10 +1,11 @@
import { Octokit } from '@octokit/rest'; 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'; import { getGhCliToken } from './gh-cli-credential.js';
export function getOctokitOrNull() { export function getOctokitOrNull() {
const auth = getGitHubAuth(); 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) { if (!token) {
return null; return null;
} }
+76 -11
View File
@@ -76,17 +76,18 @@ export function registerGitHubRoutes(app) {
app.get('/api/github/auth/status', async (_req, res) => { app.get('/api/github/auth/status', async (_req, res) => {
try { 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 { getGhCliToken } = await import('./gh-cli-credential.js');
const auth = getGitHubAuth(); const auth = getGitHubAuth();
const accounts = getGitHubAuthAccounts(); let accounts = getGitHubAuthAccounts();
const ghCliDisabled = isGhCliDisabled(); const ghCliDisabled = isGhCliDisabled();
const ghCliActive = isGhCliActive();
const ghToken = getGhCliToken(); const ghToken = getGhCliToken();
const usingOwnToken = Boolean(auth?.accessToken); const usingOwnToken = Boolean(auth?.accessToken);
let ghCliUser = null; let ghCliUser = null;
if (ghToken !== null && !ghCliDisabled && usingOwnToken) { if (ghToken !== null && !ghCliDisabled) {
try { try {
const { Octokit } = await import('@octokit/rest'); const { Octokit } = await import('@octokit/rest');
ghCliUser = await getGitHubUserSummary(new Octokit({ auth: ghToken })); ghCliUser = await getGitHubUserSummary(new Octokit({ auth: ghToken }));
@@ -94,11 +95,26 @@ export function registerGitHubRoutes(app) {
ghCliUser = null; 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) => ({ const buildGhCli = (activeUser = null) => ({
available: ghToken !== null, available: ghToken !== null,
disabled: ghCliDisabled, disabled: ghCliDisabled,
active: !usingOwnToken && ghToken !== null && !ghCliDisabled, active: ghCliCurrent,
...(!ghCliDisabled && (activeUser || ghCliUser) ? { user: activeUser || ghCliUser } : {}), ...(!ghCliDisabled && (activeUser || ghCliUser) ? { user: activeUser || ghCliUser } : {}),
}); });
@@ -119,14 +135,12 @@ export function registerGitHubRoutes(app) {
const fallback = usingOwnToken ? auth.user : null; const fallback = usingOwnToken ? auth.user : null;
const mergedUser = user || fallback; const mergedUser = user || fallback;
const ghIsActive = !usingOwnToken && ghToken !== null && !ghCliDisabled;
return res.json({ return res.json({
connected: true, connected: true,
user: mergedUser, user: mergedUser,
scope: usingOwnToken ? auth.scope : undefined, scope: ghCliCurrent ? undefined : auth?.scope,
accounts, accounts,
ghCli: buildGhCli(ghIsActive ? mergedUser : null), ghCli: buildGhCli(ghCliCurrent ? mergedUser : null),
}); });
} catch (error) { } catch (error) {
console.error('Failed to get GitHub auth status:', 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) => { app.post('/api/github/auth/activate', async (req, res) => {
try { 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 : ''; const accountId = typeof req.body?.accountId === 'string' ? req.body.accountId : '';
if (!accountId) { if (!accountId) {
return res.status(400).json({ error: 'accountId is required' }); 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); const activated = activateGitHubAuth(accountId);
if (!activated) { if (!activated) {
return res.status(404).json({ error: 'GitHub account not found' }); return res.status(404).json({ error: 'GitHub account not found' });
} }
const auth = getGitHubAuth(); const auth = getGitHubAuth();
const accounts = getGitHubAuthAccounts(); let accounts = getGitHubAuthAccounts();
if (!auth?.accessToken) { if (!auth?.accessToken) {
return res.json({ connected: false, accounts }); 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(); const octokit = getOctokitOrNull();
if (!octokit) { 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; let user = auth.user || null;
@@ -274,6 +333,12 @@ export function registerGitHubRoutes(app) {
user, user,
scope: auth.scope, scope: auth.scope,
accounts, accounts,
ghCli: {
available: ghToken !== null,
disabled: ghCliDisabled,
active: false,
...(ghCliUser ? { user: ghCliUser } : {}),
},
}); });
} catch (error) { } catch (error) {
console.error('Failed to activate GitHub account:', error); console.error('Failed to activate GitHub account:', error);