From 87d93b651bccb5e085b41925ebfff0c7e0cad82e Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 15 Apr 2026 01:23:15 -0400 Subject: [PATCH] fix(cursor): deprecate legacy bridge and harden gitlab auth --- src/cliproxy/auth/oauth-handler.ts | 30 ++-- src/cliproxy/executor/index.ts | 14 +- src/commands/cursor-command-display.ts | 51 +++--- src/commands/cursor-command.ts | 20 +++ .../cliproxy/executor-option-value.test.ts | 11 +- .../oauth-handler-paste-callback.test.ts | 31 +++- tests/unit/cursor/cursor-daemon.test.ts | 11 +- tests/unit/ui/provider-config.test.ts | 18 ++ .../components/account/add-account-dialog.tsx | 24 +-- ui/src/components/layout/app-sidebar.tsx | 7 +- ui/src/lib/i18n.ts | 162 ++++++++++++++---- ui/src/lib/provider-config.ts | 14 +- ui/src/pages/cursor.tsx | 36 +++- 13 files changed, 334 insertions(+), 95 deletions(-) create mode 100644 tests/unit/ui/provider-config.test.ts diff --git a/src/cliproxy/auth/oauth-handler.ts b/src/cliproxy/auth/oauth-handler.ts index a116e4f4..ea2381e0 100644 --- a/src/cliproxy/auth/oauth-handler.ts +++ b/src/cliproxy/auth/oauth-handler.ts @@ -69,6 +69,7 @@ import { warnPossible403Ban, } from '../account-safety'; import { ensureCliAntigravityResponsibility } from '../antigravity-responsibility'; +import { InteractivePrompt } from '../../utils/prompt'; interface PasteCallbackStartData { url?: string; @@ -153,20 +154,21 @@ function normalizeGitLabBaseUrl(baseUrl: string | undefined): string | undefined return normalized ? normalized : undefined; } -async function promptGitLabPersonalAccessToken(): Promise { - const readline = await import('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise((resolve) => { - rl.question('GitLab Personal Access Token: ', (answer) => { - rl.close(); - const token = answer.trim(); - resolve(token.length > 0 ? token : null); - }); - }); +export async function promptGitLabPersonalAccessToken(): Promise { + try { + const token = (await InteractivePrompt.password('GitLab Personal Access Token')).trim(); + return token.length > 0 ? token : null; + } catch (error) { + if ((error as Error).message.includes('TTY')) { + console.log( + fail( + 'GitLab Personal Access Token prompt requires an interactive TTY. Set the token explicitly or use Browser OAuth.' + ) + ); + return null; + } + throw error; + } } export function findNewTokenSnapshotForManualAuth( diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index 4dbe88bf..5c2c25e4 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -152,6 +152,14 @@ export function readOptionValue( return { present: true, value: next.trim(), missingValue: false }; } +export function hasGitLabTokenLoginFlag(args: string[]): boolean { + return args.includes('--gitlab-token-login') || args.includes('--token-login'); +} + +function getGitLabTokenLoginFlagName(args: string[]): '--gitlab-token-login' | '--token-login' { + return args.includes('--gitlab-token-login') ? '--gitlab-token-login' : '--token-login'; +} + /** * Execute Claude CLI with CLIProxy (main entry point) * @@ -353,7 +361,7 @@ export async function execClaudeWithCLIProxy( const addAccount = argsWithoutProxy.includes('--add'); const showAccounts = argsWithoutProxy.includes('--accounts'); const forceImport = argsWithoutProxy.includes('--import'); - const gitlabTokenLogin = argsWithoutProxy.includes('--token-login'); + const gitlabTokenLogin = hasGitLabTokenLoginFlag(argsWithoutProxy); const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy); const incognitoFlag = argsWithoutProxy.includes('--incognito'); @@ -503,7 +511,9 @@ export async function execClaudeWithCLIProxy( } if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') { - const flagName = gitlabTokenLogin ? '--token-login' : '--gitlab-url'; + const flagName = gitlabTokenLogin + ? getGitLabTokenLoginFlagName(argsWithoutProxy) + : '--gitlab-url'; console.error(fail(`${flagName} is only valid for ccs gitlab`)); process.exitCode = 1; return; diff --git a/src/commands/cursor-command-display.ts b/src/commands/cursor-command-display.ts index 3e7d4c56..eb3e7877 100644 --- a/src/commands/cursor-command-display.ts +++ b/src/commands/cursor-command-display.ts @@ -12,37 +12,46 @@ function printLines(lines: string[]): void { export function renderCursorHelp(): number { printLines([ - 'Cursor IDE Integration', + 'Legacy Cursor Compatibility', + '', + 'Deprecated: prefer CLIProxy-backed Cursor auth and account management.', + 'Supported auth path: ccs cursor --auth', + 'Supported dashboard path: ccs config -> CLIProxy -> Cursor', '', 'Usage: ccs cursor ', '', - 'Subcommands:', - ' auth Import Cursor IDE authentication token', - ' status Show integration, authentication, and daemon status', + 'Subcommands (deprecated compatibility for the local reverse-engineered bridge):', + ' auth Import Cursor IDE authentication token (deprecated)', + ' status Show legacy integration, authentication, and daemon status', ' probe Run a live authenticated runtime probe', ' models List available models', - ' start Start cursor daemon', - ' stop Stop cursor daemon', - ' enable Enable cursor integration in unified config', - ' disable Disable cursor integration in unified config', + ' start Start local cursor daemon', + ' stop Stop local cursor daemon', + ' enable Enable legacy cursor integration in unified config', + ' disable Disable legacy cursor integration in unified config', ' help Show this help message', '', - 'Runtime entry:', - ' ccs cursor [claude args] # Run Claude via the local Cursor proxy', + 'Supported CLIProxy path:', + ' ccs cursor --auth # Authenticate Cursor via CLIProxy', + ' ccs cursor --accounts # Manage CLIProxy Cursor accounts', + ' ccs cursor --config # Open CLIProxy Cursor settings', '', - 'Auth options:', - ' ccs cursor auth # Auto-detect from Cursor SQLite', + 'Legacy runtime entry (deprecated compatibility):', + ' ccs cursor [claude args] # Run Claude via the local Cursor bridge', + '', + 'Legacy auth options:', + ' ccs cursor auth # Auto-detect from Cursor SQLite (deprecated)', ' ccs cursor auth --manual --token --machine-id ', '', - 'Quick start:', - ' 1. ccs cursor enable # Enable integration', - ' 2. ccs cursor auth # Import Cursor IDE token', - ' 3. ccs cursor start # Start daemon', + 'Legacy bridge quick start:', + ' 1. ccs cursor enable # Deprecated compatibility: enable local bridge', + ' 2. ccs cursor auth # Deprecated compatibility: import Cursor IDE token', + ' 3. ccs cursor start # Start local daemon', ' 4. ccs cursor probe # Verify live runtime health', - ' 5. ccs cursor "task" # Run Claude through Cursor', + ' 5. ccs cursor "task" # Run Claude through the local bridge', ' 6. ccs cursor status # Inspect auth/daemon wiring', '', - 'Or use the web UI: ccs config -> Cursor page', + 'Web UI: ccs config -> Deprecated -> Cursor IDE', '', ]); @@ -100,7 +109,8 @@ export function renderCursorStatus( console.log(''); console.log('Client setup:'); console.log(` Raw settings: ${dirDisplay}/cursor.settings.json`); - console.log(' Runtime entry: ccs cursor [claude args]'); + console.log(' Runtime entry: ccs cursor [claude args] (deprecated compatibility)'); + console.log(' Supported auth: ccs cursor --auth'); console.log(' Live probe: ccs cursor probe'); console.log(' Status command: ccs cursor status'); console.log(' Help command: ccs cursor help'); @@ -116,7 +126,8 @@ export function renderCursorStatus( console.log(' - Enable: ccs cursor enable'); } if (!authStatus.authenticated || authStatus.expired) { - console.log(' - Auth: ccs cursor auth'); + console.log(' - Supported: ccs cursor --auth'); + console.log(' - Legacy auth: ccs cursor auth'); } if (!daemonStatus.running) { console.log(' - Start: ccs cursor start'); diff --git a/src/commands/cursor-command.ts b/src/commands/cursor-command.ts index b0f8f12d..29f5d34b 100644 --- a/src/commands/cursor-command.ts +++ b/src/commands/cursor-command.ts @@ -26,6 +26,15 @@ import { } from './cursor-command-display'; import { ok, fail, info } from '../utils/ui'; +function printLegacyCursorDeprecationNotice(): void { + console.log( + info( + 'Deprecated compatibility path. Prefer CLIProxy-backed Cursor auth via `ccs cursor --auth` or the CLIProxy dashboard.' + ) + ); + console.log(''); +} + /** * Handle cursor subcommand. */ @@ -114,6 +123,7 @@ function printAutoDetectFailure(result: { * Handle auth subcommand. */ async function handleAuth(args: string[]): Promise { + printLegacyCursorDeprecationNotice(); const manual = args.includes('--manual'); if (manual) { @@ -146,6 +156,7 @@ async function handleAuth(args: string[]): Promise { console.log(ok('Cursor credentials imported (manual mode)')); console.log(''); console.log('Next steps:'); + console.log(' 0. Preferred auth: ccs cursor --auth'); console.log(' 1. Enable integration: ccs cursor enable'); console.log(' 2. Start daemon: ccs cursor start'); return 0; @@ -168,6 +179,7 @@ async function handleAuth(args: string[]): Promise { console.log(ok('Auto-detected Cursor credentials')); console.log(''); console.log('Next steps:'); + console.log(' 0. Preferred auth: ccs cursor --auth'); console.log(' 1. Enable integration: ccs cursor enable'); console.log(' 2. Start daemon: ccs cursor start'); console.log(' 3. Check status: ccs cursor status'); @@ -185,6 +197,7 @@ async function handleAuth(args: string[]): Promise { } async function handleStatus(): Promise { + printLegacyCursorDeprecationNotice(); const cursorConfig = getCursorConfig(); const authStatus = checkAuthStatus(); const daemonStatus = await getDaemonStatus(cursorConfig.port); @@ -193,6 +206,7 @@ async function handleStatus(): Promise { } async function handleProbe(): Promise { + printLegacyCursorDeprecationNotice(); const cursorConfig = getCursorConfig(); const result = await probeCursorRuntime(cursorConfig); renderCursorProbe(result); @@ -200,6 +214,7 @@ async function handleProbe(): Promise { } async function handleModels(): Promise { + printLegacyCursorDeprecationNotice(); const cursorConfig = getCursorConfig(); const models = await getAvailableModels(cursorConfig.port); const defaultModel = getDefaultModel(); @@ -211,6 +226,7 @@ async function handleModels(): Promise { * Handle start subcommand. */ async function handleStart(): Promise { + printLegacyCursorDeprecationNotice(); const cursorConfig = getCursorConfig(); if (!cursorConfig.enabled) { @@ -248,6 +264,7 @@ async function handleStart(): Promise { * Handle stop subcommand. */ async function handleStop(): Promise { + printLegacyCursorDeprecationNotice(); console.log(info('Stopping cursor daemon...')); const result = await stopDaemon(); @@ -265,6 +282,7 @@ async function handleStop(): Promise { * Handle enable subcommand. */ async function handleEnable(): Promise { + printLegacyCursorDeprecationNotice(); mutateUnifiedConfig((config) => { if (!config.cursor) { config.cursor = { ...DEFAULT_CURSOR_CONFIG }; @@ -276,6 +294,7 @@ async function handleEnable(): Promise { console.log(ok('Cursor integration enabled')); console.log(''); console.log('Next steps:'); + console.log(' 0. Preferred auth: ccs cursor --auth'); console.log(' 1. Authenticate: ccs cursor auth'); console.log(' 2. Start daemon: ccs cursor start'); console.log(' 3. Check status: ccs cursor status'); @@ -287,6 +306,7 @@ async function handleEnable(): Promise { * Handle disable subcommand. */ async function handleDisable(): Promise { + printLegacyCursorDeprecationNotice(); mutateUnifiedConfig((config) => { if (config.cursor) { config.cursor.enabled = false; diff --git a/tests/unit/cliproxy/executor-option-value.test.ts b/tests/unit/cliproxy/executor-option-value.test.ts index 1f4971f0..b6ff278e 100644 --- a/tests/unit/cliproxy/executor-option-value.test.ts +++ b/tests/unit/cliproxy/executor-option-value.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'bun:test'; -import { readOptionValue } from '../../../src/cliproxy/executor/index'; +import { + hasGitLabTokenLoginFlag, + readOptionValue, +} from '../../../src/cliproxy/executor/index'; describe('readOptionValue', () => { it('parses split-token option values', () => { @@ -30,4 +33,10 @@ describe('readOptionValue', () => { missingValue: true, }); }); + + it('treats both GitLab token-login flags as enabled', () => { + expect(hasGitLabTokenLoginFlag(['--gitlab-token-login'])).toBe(true); + expect(hasGitLabTokenLoginFlag(['--token-login'])).toBe(true); + expect(hasGitLabTokenLoginFlag(['--gitlab-url', 'https://gitlab.example.com'])).toBe(false); + }); }); diff --git a/tests/unit/cliproxy/oauth-handler-paste-callback.test.ts b/tests/unit/cliproxy/oauth-handler-paste-callback.test.ts index 287b95f8..45873915 100644 --- a/tests/unit/cliproxy/oauth-handler-paste-callback.test.ts +++ b/tests/unit/cliproxy/oauth-handler-paste-callback.test.ts @@ -1,8 +1,9 @@ -import { afterEach, describe, expect, it } from 'bun:test'; +import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver'; +import { InteractivePrompt } from '../../../src/utils/prompt'; import { getCapturedFetchRequests, mockFetch, restoreFetch } from '../../mocks'; const remoteTarget: ProxyTarget = { @@ -233,3 +234,31 @@ describe('getCliAuthNicknameError', () => { expect(getCliAuthNicknameError('kiro', 'github-ABC123', existingAccounts, 'github-ABC123')).toBeNull(); }); }); + +describe('promptGitLabPersonalAccessToken', () => { + it('uses the masked password prompt and trims the token', async () => { + const passwordSpy = spyOn(InteractivePrompt, 'password').mockImplementation( + mock(async () => ' glpat-secret-token ') + ); + + const { promptGitLabPersonalAccessToken } = await import( + `../../../src/cliproxy/auth/oauth-handler?gitlab-pat-prompt=${Date.now()}` + ); + + await expect(promptGitLabPersonalAccessToken()).resolves.toBe('glpat-secret-token'); + expect(passwordSpy).toHaveBeenCalledWith('GitLab Personal Access Token'); + }); + + it('returns null when the masked prompt is left blank', async () => { + const passwordSpy = spyOn(InteractivePrompt, 'password').mockImplementation( + mock(async () => ' ') + ); + + const { promptGitLabPersonalAccessToken } = await import( + `../../../src/cliproxy/auth/oauth-handler?gitlab-pat-prompt-blank=${Date.now()}` + ); + + await expect(promptGitLabPersonalAccessToken()).resolves.toBeNull(); + expect(passwordSpy).toHaveBeenCalledWith('GitLab Personal Access Token'); + }); +}); diff --git a/tests/unit/cursor/cursor-daemon.test.ts b/tests/unit/cursor/cursor-daemon.test.ts index 6dd2afee..18b70d16 100644 --- a/tests/unit/cursor/cursor-daemon.test.ts +++ b/tests/unit/cursor/cursor-daemon.test.ts @@ -321,7 +321,7 @@ describe('handleCursorCommand', () => { expect(exitCode).toBe(0); expect(errors).toHaveLength(0); - expect(logs.some((line) => line.includes('Cursor IDE Integration'))).toBe(true); + expect(logs.some((line) => line.includes('Legacy Cursor Compatibility'))).toBe(true); expect(logs.some((line) => line.includes('Usage: ccs cursor '))).toBe(true); } finally { console.log = originalLog; @@ -471,7 +471,7 @@ describe('renderCursorStatus', () => { }); describe('renderCursorHelp', () => { - it('shows bare ccs cursor as the runtime entrypoint', () => { + it('marks the legacy Cursor surface deprecated while keeping compatibility guidance', () => { const originalLog = console.log; const logs: string[] = []; @@ -484,9 +484,16 @@ describe('renderCursorHelp', () => { expect(exitCode).toBe(0); expect(logs.some((line) => line.includes('Usage: ccs cursor '))).toBe(true); + expect(logs.some((line) => line.includes('Legacy Cursor Compatibility'))).toBe(true); + expect(logs.some((line) => line.includes('Deprecated: prefer CLIProxy-backed Cursor auth'))).toBe( + true + ); expect(logs.some((line) => line.includes('probe Run a live authenticated runtime probe'))).toBe( true ); + expect( + logs.some((line) => line.includes('ccs cursor --auth')) + ).toBe(true); expect( logs.some((line) => line.includes('ccs cursor [claude args]')) ).toBe(true); diff --git a/tests/unit/ui/provider-config.test.ts b/tests/unit/ui/provider-config.test.ts new file mode 100644 index 00000000..fe85c2aa --- /dev/null +++ b/tests/unit/ui/provider-config.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'bun:test'; +import { + getDeviceCodeProviderInstruction, + getProviderDisplayName, +} from '../../../ui/src/lib/provider-config'; + +describe('provider-config fallbacks', () => { + it('uses translated fallback copy for unknown providers', () => { + expect(getProviderDisplayName('not-a-provider')).toBe('Unknown provider: not-a-provider'); + expect(getProviderDisplayName(undefined)).toBe('Unknown provider: unknown'); + }); + + it('uses translated default device-code guidance for unknown providers', () => { + expect(getDeviceCodeProviderInstruction('not-a-provider')).toBe( + 'Complete the authorization in your browser.' + ); + }); +}); diff --git a/ui/src/components/account/add-account-dialog.tsx b/ui/src/components/account/add-account-dialog.tsx index 81794231..b8fcaa38 100644 --- a/ui/src/components/account/add-account-dialog.tsx +++ b/ui/src/components/account/add-account-dialog.tsx @@ -320,7 +320,7 @@ export function AddAccountDialog({ return; } if (isGitLab && gitlabAuthMode === 'pat' && !gitlabPersonalAccessTokenTrimmed) { - setLocalError('GitLab Personal Access Token is required for PAT login.'); + setLocalError(t('addAccountDialog.gitlabPatRequired')); return; } wasAuthenticatingRef.current = true; @@ -538,7 +538,7 @@ export function AddAccountDialog({ {isGitLab && !showAuthUI && (
- +

- Use Browser OAuth for gitlab.com, or PAT for self-hosted and admin-managed setups. + {t('addAccountDialog.gitlabAuthHint')}

- +

- Optional. Leave blank for gitlab.com, or set your self-hosted GitLab base URL. + {t('addAccountDialog.gitlabUrlHint')}

{gitlabAuthMode === 'pat' && (
- +

- Token must include at least api and{' '} + {t('addAccountDialog.gitlabPatHint')} api and{' '} read_user scopes.

diff --git a/ui/src/components/layout/app-sidebar.tsx b/ui/src/components/layout/app-sidebar.tsx index ac2265d4..1d9c01e0 100644 --- a/ui/src/components/layout/app-sidebar.tsx +++ b/ui/src/components/layout/app-sidebar.tsx @@ -103,7 +103,6 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] { ], }, { path: '/copilot', icon: Github, label: t('nav.githubCopilot') }, - { path: '/cursor', iconSrc: '/assets/sidebar/cursor.svg', label: t('nav.cursorIde') }, { path: '/accounts', icon: Users, @@ -124,6 +123,12 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] { { path: '/droid', icon: TerminalSquare, label: t('nav.factoryDroid') }, ], }, + { + title: t('nav.deprecated'), + items: [ + { path: '/cursor', iconSrc: '/assets/sidebar/cursor.svg', label: t('nav.cursorIde') }, + ], + }, { title: t('nav.system'), items: [ diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 6eb2d086..fdb803ff 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -1,6 +1,6 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; -import { getInitialLocale } from '@/lib/locales'; +import { getInitialLocale } from './locales'; const resources = { en: { @@ -30,6 +30,7 @@ const resources = { allAccounts: 'All Accounts', sharedData: 'Shared Data', compatibleClis: 'Compatible', + deprecated: 'Deprecated', factoryDroid: 'Factory Droid', system: 'System', health: 'Health', @@ -687,6 +688,20 @@ const resources = { 'Power user mode is unavailable. Complete the required provider safety step and retry.', authMethod: 'Auth Method', selectKiroAuthMethod: 'Select Kiro auth method', + gitlabAuthMethod: 'GitLab auth method', + selectGitlabAuthMethod: 'Select GitLab auth method', + gitlabAuthOAuth: 'Browser OAuth', + gitlabAuthPat: 'Personal Access Token', + gitlabAuthHint: + 'Use Browser OAuth for gitlab.com, or PAT for self-hosted and admin-managed setups.', + gitlabUrl: 'GitLab URL', + gitlabUrlPlaceholder: 'https://gitlab.com', + gitlabUrlHint: + 'Optional. Leave blank for gitlab.com, or set your self-hosted GitLab base URL.', + gitlabPat: 'Personal Access Token', + gitlabPatPlaceholder: 'glpat-...', + gitlabPatHint: 'Token must include at least', + gitlabPatRequired: 'GitLab Personal Access Token is required for PAT login.', nicknameRequired: 'Nickname (required)', nicknameOptional: 'Nickname (optional)', nicknamePlaceholder: 'e.g., work, personal', @@ -1257,11 +1272,17 @@ const resources = { cursorPage: { title: 'Cursor', beta: 'Beta', - subtitle: 'Dedicated Cursor integration controls', - unofficialTitle: 'Unofficial API - Use at Your Own Risk', - unofficialItem1: 'Reverse-engineered integration may break anytime', - unofficialItem2: 'Abuse or excessive usage may risk account restrictions', - unofficialItem3: 'No warranty, no responsibility from CCS', + deprecated: 'Deprecated', + subtitle: 'Deprecated local bridge for legacy Cursor setups', + unofficialTitle: 'Deprecated legacy bridge - prefer CLIProxy-backed Cursor', + unofficialItem1: 'Supported path: CLIProxy-backed Cursor auth and account management', + unofficialItem2: 'This reverse-engineered local bridge remains only for existing setups', + unofficialItem3: 'CCS provides no warranty for this legacy path', + supportedPathTitle: 'Supported path', + supportedPathDesc: + 'Use CLIProxy-backed Cursor auth and account management for new setups. Keep the legacy bridge below only if you still rely on the old local daemon flow.', + startCliproxyAuth: 'Start CLIProxy Cursor Auth', + openCliproxyCursor: 'Open CLIProxy Cursor', integration: 'Integration', authentication: 'Authentication', daemon: 'Daemon', @@ -1272,11 +1293,11 @@ const resources = { notConnected: 'Not connected', running: 'Running', stopped: 'Stopped', - actions: 'Actions', + actions: 'Legacy Actions', disableIntegration: 'Disable Integration', enableIntegration: 'Enable Integration', - autoDetectAuth: 'Auto-detect Auth', - manualAuthImport: 'Manual Auth Import', + autoDetectAuth: 'Legacy IDE Auto-detect', + manualAuthImport: 'Legacy Manual Import', stopDaemon: 'Stop Daemon', startDaemon: 'Start Daemon', port: 'Port', @@ -2206,6 +2227,9 @@ const resources = { profileExportDownloaded: 'Profile export downloaded', profileImportFailed: 'Failed to import profile bundle', }, + providerConfig: { + defaultDeviceCodeInstruction: 'Complete the authorization in your browser.', + }, // ======================================== // Domain 7: Profiles / Settings / Pages @@ -2475,6 +2499,7 @@ const resources = { allAccounts: '全部账号', sharedData: '共享数据', compatibleClis: '兼容', + deprecated: '已弃用', factoryDroid: 'Factory Droid', system: '系统', health: '健康', @@ -3079,6 +3104,18 @@ const resources = { powerUserUnavailableRetry: '高级用户模式不可用。请完成当前提供商要求的安全步骤后重试。', authMethod: '认证方式', selectKiroAuthMethod: '选择 Kiro 认证方式', + gitlabAuthMethod: 'GitLab 认证方式', + selectGitlabAuthMethod: '选择 GitLab 认证方式', + gitlabAuthOAuth: '浏览器 OAuth', + gitlabAuthPat: '个人访问令牌', + gitlabAuthHint: 'gitlab.com 推荐使用浏览器 OAuth;自托管或管理员管理场景可使用 PAT。', + gitlabUrl: 'GitLab URL', + gitlabUrlPlaceholder: 'https://gitlab.com', + gitlabUrlHint: '可选。留空表示 gitlab.com,或填写你的自托管 GitLab 基础 URL。', + gitlabPat: '个人访问令牌', + gitlabPatPlaceholder: 'glpat-...', + gitlabPatHint: '令牌至少需要包含', + gitlabPatRequired: 'PAT 登录需要提供 GitLab 个人访问令牌。', nicknameRequired: '昵称(必填)', nicknameOptional: '昵称(选填)', nicknamePlaceholder: '例如:工作、个人', @@ -3628,11 +3665,17 @@ const resources = { cursorPage: { title: 'Cursor', beta: 'Beta', - subtitle: 'Cursor 集成专用控制面板', - unofficialTitle: '非官方 API - 风险自负', - unofficialItem1: '逆向集成可能随时失效', - unofficialItem2: '滥用或过量使用可能导致账号受限', - unofficialItem3: 'CCS 不提供担保,也不承担责任', + deprecated: '已弃用', + subtitle: '面向旧版 Cursor 使用场景的已弃用本地桥接', + unofficialTitle: '旧版桥接已弃用,优先使用 CLIProxy 支持的 Cursor', + unofficialItem1: '推荐路径:使用 CLIProxy 管理 Cursor 认证与账号', + unofficialItem2: '这个逆向本地桥接仅为仍在使用旧流程的用户保留', + unofficialItem3: 'CCS 不为这个旧路径提供担保,也不承担责任', + supportedPathTitle: '推荐路径', + supportedPathDesc: + '新的配置请使用 CLIProxy 支持的 Cursor 认证与账号管理。只有在你仍依赖旧的本地守护进程流程时,才继续使用下方旧版桥接。', + startCliproxyAuth: '启动 CLIProxy Cursor 认证', + openCliproxyCursor: '打开 CLIProxy Cursor', integration: '集成', authentication: '认证', daemon: '守护进程', @@ -3643,11 +3686,11 @@ const resources = { notConnected: '未连接', running: '运行中', stopped: '已停止', - actions: '操作', + actions: '旧版操作', disableIntegration: '禁用集成', enableIntegration: '启用集成', - autoDetectAuth: '自动检测认证', - manualAuthImport: '手动导入认证', + autoDetectAuth: '旧版 IDE 自动检测', + manualAuthImport: '旧版手动导入', stopDaemon: '停止守护进程', startDaemon: '启动守护进程', port: '端口', @@ -4543,6 +4586,9 @@ const resources = { profileExportDownloaded: '配置导出已下载', profileImportFailed: '导入配置包失败', }, + providerConfig: { + defaultDeviceCodeInstruction: '请在浏览器中完成授权。', + }, profileEditorSections: { imageAnalysis: '图片分析', loadingImageSettings: '加载图片设置中...', @@ -4805,6 +4851,7 @@ const resources = { allAccounts: 'Tất cả tài khoản', sharedData: 'Dữ liệu dùng chung', compatibleClis: 'Tương thích', + deprecated: 'Đã ngừng ưu tiên', factoryDroid: 'Factory Droid', system: 'Hệ thống', health: 'Sức khỏe', @@ -5474,6 +5521,20 @@ const resources = { 'Chế độ power user hiện không khả dụng. Hãy hoàn tất bước an toàn bắt buộc của nhà cung cấp rồi thử lại.', authMethod: 'Phương thức xác thực', selectKiroAuthMethod: 'Chọn phương thức xác thực Kiro', + gitlabAuthMethod: 'Phương thức xác thực GitLab', + selectGitlabAuthMethod: 'Chọn phương thức xác thực GitLab', + gitlabAuthOAuth: 'OAuth trên trình duyệt', + gitlabAuthPat: 'Personal Access Token', + gitlabAuthHint: + 'Dùng OAuth trên trình duyệt cho gitlab.com, hoặc PAT cho GitLab tự lưu trữ và môi trường do quản trị viên quản lý.', + gitlabUrl: 'GitLab URL', + gitlabUrlPlaceholder: 'https://gitlab.com', + gitlabUrlHint: + 'Tùy chọn. Để trống cho gitlab.com, hoặc nhập URL GitLab tự lưu trữ của bạn.', + gitlabPat: 'Personal Access Token', + gitlabPatPlaceholder: 'glpat-...', + gitlabPatHint: 'Token phải có ít nhất các scope', + gitlabPatRequired: 'PAT login yêu cầu GitLab Personal Access Token.', nicknameRequired: 'Biệt danh (bắt buộc)', nicknameOptional: 'Biệt danh (tùy chọn)', nicknamePlaceholder: 'ví dụ: công việc, cá nhân', @@ -6051,11 +6112,17 @@ const resources = { cursorPage: { title: 'Cursor', beta: 'Beta', - subtitle: 'Bảng điều khiển tích hợp Cursor', - unofficialTitle: 'API không chính thức - Bạn phải tự chịu rủi ro khi sử dụng', - unofficialItem1: 'Tích hợp thiết kế ngược có thể bị hỏng bất cứ lúc nào', - unofficialItem2: 'Lạm dụng hoặc sử dụng quá mức có thể có nguy cơ bị hạn chế tài khoản', - unofficialItem3: 'Không bảo hành, không chịu trách nhiệm từ CCS', + deprecated: 'Đã ngừng ưu tiên', + subtitle: 'Cầu nối cục bộ đã bị ngừng ưu tiên cho các thiết lập Cursor cũ', + unofficialTitle: 'Cầu nối cũ đã bị ngừng ưu tiên - hãy dùng Cursor qua CLIProxy', + unofficialItem1: 'Đường dẫn được hỗ trợ: xác thực và quản lý tài khoản Cursor qua CLIProxy', + unofficialItem2: 'Cầu nối cục bộ reverse-engineered này chỉ còn dành cho các thiết lập cũ', + unofficialItem3: 'CCS không bảo hành và không chịu trách nhiệm cho đường dẫn cũ này', + supportedPathTitle: 'Đường dẫn được hỗ trợ', + supportedPathDesc: + 'Hãy dùng xác thực và quản lý tài khoản Cursor qua CLIProxy cho các thiết lập mới. Chỉ giữ cầu nối cũ bên dưới nếu bạn vẫn phụ thuộc vào luồng daemon cục bộ trước đây.', + startCliproxyAuth: 'Bắt đầu xác thực Cursor qua CLIProxy', + openCliproxyCursor: 'Mở Cursor trong CLIProxy', integration: 'Tích hợp', authentication: 'Xác thực', daemon: 'Daemon', @@ -6066,11 +6133,11 @@ const resources = { notConnected: 'Chưa kết nối', running: 'Đang chạy', stopped: 'Đã dừng', - actions: 'Hành động', + actions: 'Hành động cũ', disableIntegration: 'Vô hiệu hóa tích hợp', enableIntegration: 'Kích hoạt tích hợp', - autoDetectAuth: 'Tự động phát hiện xác thực', - manualAuthImport: 'Nhập xác thực thủ công', + autoDetectAuth: 'Tự dò IDE cũ', + manualAuthImport: 'Nhập thủ công kiểu cũ', stopDaemon: 'Dừng Daemon', startDaemon: 'Khởi động Daemon', port: 'Cổng', @@ -6978,6 +7045,9 @@ const resources = { profileExportDownloaded: 'Đã tải xuống xuất hồ sơ', profileImportFailed: 'Không nhập được gói hồ sơ', }, + providerConfig: { + defaultDeviceCodeInstruction: 'Hoàn tất việc cấp quyền trong trình duyệt của bạn.', + }, profileEditorSections: { imageAnalysis: 'Phân tích hình ảnh', loadingImageSettings: 'Đang tải cài đặt hình ảnh...', @@ -7243,6 +7313,7 @@ const resources = { allAccounts: 'すべてのアカウント', sharedData: '共有データ', compatibleClis: '互換', + deprecated: '非推奨', factoryDroid: 'Factory Droid', system: 'システム', health: 'ヘルス', @@ -7911,6 +7982,20 @@ const resources = { 'パワーユーザーモードは利用できません。必要なプロバイダーの安全確認を完了してから再試行してください。', authMethod: '認証方法', selectKiroAuthMethod: 'Kiro の認証方法を選択', + gitlabAuthMethod: 'GitLab 認証方法', + selectGitlabAuthMethod: 'GitLab 認証方法を選択', + gitlabAuthOAuth: 'ブラウザー OAuth', + gitlabAuthPat: 'Personal Access Token', + gitlabAuthHint: + 'gitlab.com ではブラウザー OAuth を使い、自前ホストや管理者運用環境では PAT を使ってください。', + gitlabUrl: 'GitLab URL', + gitlabUrlPlaceholder: 'https://gitlab.com', + gitlabUrlHint: + '任意です。gitlab.com を使う場合は空欄のままにし、自前ホストの場合はベース URL を指定してください。', + gitlabPat: 'Personal Access Token', + gitlabPatPlaceholder: 'glpat-...', + gitlabPatHint: 'トークンには少なくとも次のスコープが必要です:', + gitlabPatRequired: 'PAT ログインには GitLab Personal Access Token が必要です。', nicknameRequired: 'ニックネーム(必須)', nicknameOptional: 'ニックネーム(任意)', nicknamePlaceholder: '例: work, personal', @@ -8494,12 +8579,18 @@ const resources = { cursorPage: { title: 'Cursor', beta: 'ベータ', - subtitle: 'Cursor 専用の連携設定', - unofficialTitle: '非公式API - 自己責任で利用', - unofficialItem1: - 'リバースエンジニアリング連携のため、いつでも利用不能になる可能性があります', - unofficialItem2: '不正利用や過度な利用により、アカウントが制限されるおそれがあります', - unofficialItem3: 'CCSは保証も責任も負いません', + deprecated: '非推奨', + subtitle: '旧 Cursor セットアップ向けの非推奨ローカルブリッジ', + unofficialTitle: 'レガシーブリッジは非推奨です。CLIProxy 管理の Cursor を優先してください', + unofficialItem1: '推奨パス: CLIProxy で Cursor の認証とアカウント管理を行う', + unofficialItem2: + 'このリバースエンジニアリングされたローカルブリッジは、既存セットアップ向けの互換用です', + unofficialItem3: 'このレガシーパスについて CCS は保証も責任も負いません', + supportedPathTitle: '推奨パス', + supportedPathDesc: + '新しいセットアップでは、CLIProxy 管理の Cursor 認証とアカウント管理を使ってください。旧来のローカルデーモンに依存している場合のみ、下のレガシーブリッジを使い続けてください。', + startCliproxyAuth: 'CLIProxy で Cursor 認証を開始', + openCliproxyCursor: 'CLIProxy の Cursor を開く', integration: '連携', authentication: '認証', daemon: 'デーモン', @@ -8510,11 +8601,11 @@ const resources = { notConnected: '未接続', running: '稼働中', stopped: '停止中', - actions: '操作', + actions: 'レガシー操作', disableIntegration: '連携を無効化', enableIntegration: '連携を有効化', - autoDetectAuth: '認証を自動検出', - manualAuthImport: '認証情報を手動インポート', + autoDetectAuth: '旧 IDE から自動検出', + manualAuthImport: '旧方式で手動インポート', stopDaemon: 'デーモンを停止', startDaemon: 'デーモンを起動', port: 'ポート', @@ -9648,6 +9739,9 @@ const resources = { profileExportDownloaded: 'プロファイルエクスポートをダウンロードしました', profileImportFailed: 'プロファイルバンドルのインポートに失敗しました', }, + providerConfig: { + defaultDeviceCodeInstruction: 'ブラウザーで認証を完了してください。', + }, updatesSpotlight: { openUpdatesCenter: '更新センターを開く', }, diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts index ab8ee75e..15ef36ce 100644 --- a/ui/src/lib/provider-config.ts +++ b/ui/src/lib/provider-config.ts @@ -10,6 +10,7 @@ import { getProvidersByOAuthFlow, } from '../../../src/cliproxy/provider-capabilities'; import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/cliproxy/ai-providers'; +import i18n from './i18n'; // Monorepo contract: UI consumes provider capability constants directly from backend // to enforce one source of truth and prevent provider drift across surfaces. @@ -253,9 +254,9 @@ const PROVIDER_NAMES: Record = { export function getProviderDisplayName(provider: unknown): string { const normalized = normalizeProviderInput(provider); if (!normalized) { - return 'Unknown provider'; // TODO i18n: missing key + return i18n.t('toasts.providerUnknown', { provider: 'unknown' }); } - return PROVIDER_NAMES[normalized] || String(provider); + return PROVIDER_NAMES[normalized] || i18n.t('toasts.providerUnknown', { provider: normalized }); } /** Map provider to user-facing short description */ @@ -297,12 +298,12 @@ export function isDeviceCodeProvider(provider: unknown): boolean { export function getDeviceCodeProviderDisplayName(provider: unknown): string { const normalized = normalizeProviderInput(provider); if (!normalized) { - return 'Unknown provider'; // TODO i18n: missing key + return i18n.t('toasts.providerUnknown', { provider: 'unknown' }); } if (isValidProvider(normalized)) { return DEVICE_CODE_PROVIDER_DISPLAY_NAMES[normalized] || getProviderDisplayName(normalized); } - return String(provider); + return i18n.t('toasts.providerUnknown', { provider: normalized }); } /** Provider-specific helper text for device-code dialog. */ @@ -310,10 +311,11 @@ export function getDeviceCodeProviderInstruction(provider: unknown): string { const normalized = normalizeProviderInput(provider); if (isValidProvider(normalized)) { return ( - DEVICE_CODE_PROVIDER_INSTRUCTIONS[normalized] || 'Complete the authorization in your browser.' // TODO i18n: missing key + DEVICE_CODE_PROVIDER_INSTRUCTIONS[normalized] || + i18n.t('providerConfig.defaultDeviceCodeInstruction') ); } - return 'Complete the authorization in your browser.'; // TODO i18n: missing key + return i18n.t('providerConfig.defaultDeviceCodeInstruction'); } /** Kiro auth methods exposed in CCS UI (aligned with CLIProxyAPIPlus support). */ diff --git a/ui/src/pages/cursor.tsx b/ui/src/pages/cursor.tsx index dffa5262..1767e3ad 100644 --- a/ui/src/pages/cursor.tsx +++ b/ui/src/pages/cursor.tsx @@ -5,6 +5,7 @@ import { useMemo, useState, type ElementType } from 'react'; import { toast } from 'sonner'; +import { useNavigate } from 'react-router-dom'; import { AlertTriangle, CheckCircle2, @@ -289,6 +290,7 @@ function StatusItem({ export function CursorPage() { const { t } = useTranslation(); + const navigate = useNavigate(); const { status, statusLoading, @@ -733,9 +735,9 @@ export function CursorPage() {

{t('cursorPage.title')}

- {t('cursorPage.beta')} + {t('cursorPage.deprecated')} {integrationBadge}
@@ -770,6 +772,36 @@ export function CursorPage() { +
+
+

+ {t('cursorPage.supportedPathTitle')} +

+

+ {t('cursorPage.supportedPathDesc')} +

+
+
+ + +
+
+