mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-08 06:17:06 +00:00
fix(cursor): deprecate legacy bridge and harden gitlab auth
This commit is contained in:
@@ -69,6 +69,7 @@ import {
|
|||||||
warnPossible403Ban,
|
warnPossible403Ban,
|
||||||
} from '../account-safety';
|
} from '../account-safety';
|
||||||
import { ensureCliAntigravityResponsibility } from '../antigravity-responsibility';
|
import { ensureCliAntigravityResponsibility } from '../antigravity-responsibility';
|
||||||
|
import { InteractivePrompt } from '../../utils/prompt';
|
||||||
|
|
||||||
interface PasteCallbackStartData {
|
interface PasteCallbackStartData {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -153,20 +154,21 @@ function normalizeGitLabBaseUrl(baseUrl: string | undefined): string | undefined
|
|||||||
return normalized ? normalized : undefined;
|
return normalized ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptGitLabPersonalAccessToken(): Promise<string | null> {
|
export async function promptGitLabPersonalAccessToken(): Promise<string | null> {
|
||||||
const readline = await import('readline');
|
try {
|
||||||
const rl = readline.createInterface({
|
const token = (await InteractivePrompt.password('GitLab Personal Access Token')).trim();
|
||||||
input: process.stdin,
|
return token.length > 0 ? token : null;
|
||||||
output: process.stdout,
|
} catch (error) {
|
||||||
});
|
if ((error as Error).message.includes('TTY')) {
|
||||||
|
console.log(
|
||||||
return new Promise<string | null>((resolve) => {
|
fail(
|
||||||
rl.question('GitLab Personal Access Token: ', (answer) => {
|
'GitLab Personal Access Token prompt requires an interactive TTY. Set the token explicitly or use Browser OAuth.'
|
||||||
rl.close();
|
)
|
||||||
const token = answer.trim();
|
);
|
||||||
resolve(token.length > 0 ? token : null);
|
return null;
|
||||||
});
|
}
|
||||||
});
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findNewTokenSnapshotForManualAuth(
|
export function findNewTokenSnapshotForManualAuth(
|
||||||
|
|||||||
@@ -152,6 +152,14 @@ export function readOptionValue(
|
|||||||
return { present: true, value: next.trim(), missingValue: false };
|
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)
|
* Execute Claude CLI with CLIProxy (main entry point)
|
||||||
*
|
*
|
||||||
@@ -353,7 +361,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
const addAccount = argsWithoutProxy.includes('--add');
|
const addAccount = argsWithoutProxy.includes('--add');
|
||||||
const showAccounts = argsWithoutProxy.includes('--accounts');
|
const showAccounts = argsWithoutProxy.includes('--accounts');
|
||||||
const forceImport = argsWithoutProxy.includes('--import');
|
const forceImport = argsWithoutProxy.includes('--import');
|
||||||
const gitlabTokenLogin = argsWithoutProxy.includes('--token-login');
|
const gitlabTokenLogin = hasGitLabTokenLoginFlag(argsWithoutProxy);
|
||||||
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
|
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
|
||||||
|
|
||||||
const incognitoFlag = argsWithoutProxy.includes('--incognito');
|
const incognitoFlag = argsWithoutProxy.includes('--incognito');
|
||||||
@@ -503,7 +511,9 @@ export async function execClaudeWithCLIProxy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') {
|
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`));
|
console.error(fail(`${flagName} is only valid for ccs gitlab`));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,37 +12,46 @@ function printLines(lines: string[]): void {
|
|||||||
|
|
||||||
export function renderCursorHelp(): number {
|
export function renderCursorHelp(): number {
|
||||||
printLines([
|
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 <subcommand>',
|
'Usage: ccs cursor <subcommand>',
|
||||||
'',
|
'',
|
||||||
'Subcommands:',
|
'Subcommands (deprecated compatibility for the local reverse-engineered bridge):',
|
||||||
' auth Import Cursor IDE authentication token',
|
' auth Import Cursor IDE authentication token (deprecated)',
|
||||||
' status Show integration, authentication, and daemon status',
|
' status Show legacy integration, authentication, and daemon status',
|
||||||
' probe Run a live authenticated runtime probe',
|
' probe Run a live authenticated runtime probe',
|
||||||
' models List available models',
|
' models List available models',
|
||||||
' start Start cursor daemon',
|
' start Start local cursor daemon',
|
||||||
' stop Stop cursor daemon',
|
' stop Stop local cursor daemon',
|
||||||
' enable Enable cursor integration in unified config',
|
' enable Enable legacy cursor integration in unified config',
|
||||||
' disable Disable cursor integration in unified config',
|
' disable Disable legacy cursor integration in unified config',
|
||||||
' help Show this help message',
|
' help Show this help message',
|
||||||
'',
|
'',
|
||||||
'Runtime entry:',
|
'Supported CLIProxy path:',
|
||||||
' ccs cursor [claude args] # Run Claude via the local Cursor proxy',
|
' ccs cursor --auth # Authenticate Cursor via CLIProxy',
|
||||||
|
' ccs cursor --accounts # Manage CLIProxy Cursor accounts',
|
||||||
|
' ccs cursor --config # Open CLIProxy Cursor settings',
|
||||||
'',
|
'',
|
||||||
'Auth options:',
|
'Legacy runtime entry (deprecated compatibility):',
|
||||||
' ccs cursor auth # Auto-detect from Cursor SQLite',
|
' 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 <t> --machine-id <id>',
|
' ccs cursor auth --manual --token <t> --machine-id <id>',
|
||||||
'',
|
'',
|
||||||
'Quick start:',
|
'Legacy bridge quick start:',
|
||||||
' 1. ccs cursor enable # Enable integration',
|
' 1. ccs cursor enable # Deprecated compatibility: enable local bridge',
|
||||||
' 2. ccs cursor auth # Import Cursor IDE token',
|
' 2. ccs cursor auth # Deprecated compatibility: import Cursor IDE token',
|
||||||
' 3. ccs cursor start # Start daemon',
|
' 3. ccs cursor start # Start local daemon',
|
||||||
' 4. ccs cursor probe # Verify live runtime health',
|
' 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',
|
' 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('');
|
||||||
console.log('Client setup:');
|
console.log('Client setup:');
|
||||||
console.log(` Raw settings: ${dirDisplay}/cursor.settings.json`);
|
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(' Live probe: ccs cursor probe');
|
||||||
console.log(' Status command: ccs cursor status');
|
console.log(' Status command: ccs cursor status');
|
||||||
console.log(' Help command: ccs cursor help');
|
console.log(' Help command: ccs cursor help');
|
||||||
@@ -116,7 +126,8 @@ export function renderCursorStatus(
|
|||||||
console.log(' - Enable: ccs cursor enable');
|
console.log(' - Enable: ccs cursor enable');
|
||||||
}
|
}
|
||||||
if (!authStatus.authenticated || authStatus.expired) {
|
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) {
|
if (!daemonStatus.running) {
|
||||||
console.log(' - Start: ccs cursor start');
|
console.log(' - Start: ccs cursor start');
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ import {
|
|||||||
} from './cursor-command-display';
|
} from './cursor-command-display';
|
||||||
import { ok, fail, info } from '../utils/ui';
|
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.
|
* Handle cursor subcommand.
|
||||||
*/
|
*/
|
||||||
@@ -114,6 +123,7 @@ function printAutoDetectFailure(result: {
|
|||||||
* Handle auth subcommand.
|
* Handle auth subcommand.
|
||||||
*/
|
*/
|
||||||
async function handleAuth(args: string[]): Promise<number> {
|
async function handleAuth(args: string[]): Promise<number> {
|
||||||
|
printLegacyCursorDeprecationNotice();
|
||||||
const manual = args.includes('--manual');
|
const manual = args.includes('--manual');
|
||||||
|
|
||||||
if (manual) {
|
if (manual) {
|
||||||
@@ -146,6 +156,7 @@ async function handleAuth(args: string[]): Promise<number> {
|
|||||||
console.log(ok('Cursor credentials imported (manual mode)'));
|
console.log(ok('Cursor credentials imported (manual mode)'));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Next steps:');
|
console.log('Next steps:');
|
||||||
|
console.log(' 0. Preferred auth: ccs cursor --auth');
|
||||||
console.log(' 1. Enable integration: ccs cursor enable');
|
console.log(' 1. Enable integration: ccs cursor enable');
|
||||||
console.log(' 2. Start daemon: ccs cursor start');
|
console.log(' 2. Start daemon: ccs cursor start');
|
||||||
return 0;
|
return 0;
|
||||||
@@ -168,6 +179,7 @@ async function handleAuth(args: string[]): Promise<number> {
|
|||||||
console.log(ok('Auto-detected Cursor credentials'));
|
console.log(ok('Auto-detected Cursor credentials'));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Next steps:');
|
console.log('Next steps:');
|
||||||
|
console.log(' 0. Preferred auth: ccs cursor --auth');
|
||||||
console.log(' 1. Enable integration: ccs cursor enable');
|
console.log(' 1. Enable integration: ccs cursor enable');
|
||||||
console.log(' 2. Start daemon: ccs cursor start');
|
console.log(' 2. Start daemon: ccs cursor start');
|
||||||
console.log(' 3. Check status: ccs cursor status');
|
console.log(' 3. Check status: ccs cursor status');
|
||||||
@@ -185,6 +197,7 @@ async function handleAuth(args: string[]): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleStatus(): Promise<number> {
|
async function handleStatus(): Promise<number> {
|
||||||
|
printLegacyCursorDeprecationNotice();
|
||||||
const cursorConfig = getCursorConfig();
|
const cursorConfig = getCursorConfig();
|
||||||
const authStatus = checkAuthStatus();
|
const authStatus = checkAuthStatus();
|
||||||
const daemonStatus = await getDaemonStatus(cursorConfig.port);
|
const daemonStatus = await getDaemonStatus(cursorConfig.port);
|
||||||
@@ -193,6 +206,7 @@ async function handleStatus(): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleProbe(): Promise<number> {
|
async function handleProbe(): Promise<number> {
|
||||||
|
printLegacyCursorDeprecationNotice();
|
||||||
const cursorConfig = getCursorConfig();
|
const cursorConfig = getCursorConfig();
|
||||||
const result = await probeCursorRuntime(cursorConfig);
|
const result = await probeCursorRuntime(cursorConfig);
|
||||||
renderCursorProbe(result);
|
renderCursorProbe(result);
|
||||||
@@ -200,6 +214,7 @@ async function handleProbe(): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleModels(): Promise<number> {
|
async function handleModels(): Promise<number> {
|
||||||
|
printLegacyCursorDeprecationNotice();
|
||||||
const cursorConfig = getCursorConfig();
|
const cursorConfig = getCursorConfig();
|
||||||
const models = await getAvailableModels(cursorConfig.port);
|
const models = await getAvailableModels(cursorConfig.port);
|
||||||
const defaultModel = getDefaultModel();
|
const defaultModel = getDefaultModel();
|
||||||
@@ -211,6 +226,7 @@ async function handleModels(): Promise<number> {
|
|||||||
* Handle start subcommand.
|
* Handle start subcommand.
|
||||||
*/
|
*/
|
||||||
async function handleStart(): Promise<number> {
|
async function handleStart(): Promise<number> {
|
||||||
|
printLegacyCursorDeprecationNotice();
|
||||||
const cursorConfig = getCursorConfig();
|
const cursorConfig = getCursorConfig();
|
||||||
|
|
||||||
if (!cursorConfig.enabled) {
|
if (!cursorConfig.enabled) {
|
||||||
@@ -248,6 +264,7 @@ async function handleStart(): Promise<number> {
|
|||||||
* Handle stop subcommand.
|
* Handle stop subcommand.
|
||||||
*/
|
*/
|
||||||
async function handleStop(): Promise<number> {
|
async function handleStop(): Promise<number> {
|
||||||
|
printLegacyCursorDeprecationNotice();
|
||||||
console.log(info('Stopping cursor daemon...'));
|
console.log(info('Stopping cursor daemon...'));
|
||||||
|
|
||||||
const result = await stopDaemon();
|
const result = await stopDaemon();
|
||||||
@@ -265,6 +282,7 @@ async function handleStop(): Promise<number> {
|
|||||||
* Handle enable subcommand.
|
* Handle enable subcommand.
|
||||||
*/
|
*/
|
||||||
async function handleEnable(): Promise<number> {
|
async function handleEnable(): Promise<number> {
|
||||||
|
printLegacyCursorDeprecationNotice();
|
||||||
mutateUnifiedConfig((config) => {
|
mutateUnifiedConfig((config) => {
|
||||||
if (!config.cursor) {
|
if (!config.cursor) {
|
||||||
config.cursor = { ...DEFAULT_CURSOR_CONFIG };
|
config.cursor = { ...DEFAULT_CURSOR_CONFIG };
|
||||||
@@ -276,6 +294,7 @@ async function handleEnable(): Promise<number> {
|
|||||||
console.log(ok('Cursor integration enabled'));
|
console.log(ok('Cursor integration enabled'));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Next steps:');
|
console.log('Next steps:');
|
||||||
|
console.log(' 0. Preferred auth: ccs cursor --auth');
|
||||||
console.log(' 1. Authenticate: ccs cursor auth');
|
console.log(' 1. Authenticate: ccs cursor auth');
|
||||||
console.log(' 2. Start daemon: ccs cursor start');
|
console.log(' 2. Start daemon: ccs cursor start');
|
||||||
console.log(' 3. Check status: ccs cursor status');
|
console.log(' 3. Check status: ccs cursor status');
|
||||||
@@ -287,6 +306,7 @@ async function handleEnable(): Promise<number> {
|
|||||||
* Handle disable subcommand.
|
* Handle disable subcommand.
|
||||||
*/
|
*/
|
||||||
async function handleDisable(): Promise<number> {
|
async function handleDisable(): Promise<number> {
|
||||||
|
printLegacyCursorDeprecationNotice();
|
||||||
mutateUnifiedConfig((config) => {
|
mutateUnifiedConfig((config) => {
|
||||||
if (config.cursor) {
|
if (config.cursor) {
|
||||||
config.cursor.enabled = false;
|
config.cursor.enabled = false;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { describe, expect, it } from 'bun:test';
|
import { describe, expect, it } from 'bun:test';
|
||||||
import { readOptionValue } from '../../../src/cliproxy/executor/index';
|
import {
|
||||||
|
hasGitLabTokenLoginFlag,
|
||||||
|
readOptionValue,
|
||||||
|
} from '../../../src/cliproxy/executor/index';
|
||||||
|
|
||||||
describe('readOptionValue', () => {
|
describe('readOptionValue', () => {
|
||||||
it('parses split-token option values', () => {
|
it('parses split-token option values', () => {
|
||||||
@@ -30,4 +33,10 @@ describe('readOptionValue', () => {
|
|||||||
missingValue: true,
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver';
|
import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver';
|
||||||
|
import { InteractivePrompt } from '../../../src/utils/prompt';
|
||||||
import { getCapturedFetchRequests, mockFetch, restoreFetch } from '../../mocks';
|
import { getCapturedFetchRequests, mockFetch, restoreFetch } from '../../mocks';
|
||||||
|
|
||||||
const remoteTarget: ProxyTarget = {
|
const remoteTarget: ProxyTarget = {
|
||||||
@@ -233,3 +234,31 @@ describe('getCliAuthNicknameError', () => {
|
|||||||
expect(getCliAuthNicknameError('kiro', 'github-ABC123', existingAccounts, 'github-ABC123')).toBeNull();
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ describe('handleCursorCommand', () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(errors).toHaveLength(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 <subcommand>'))).toBe(true);
|
expect(logs.some((line) => line.includes('Usage: ccs cursor <subcommand>'))).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
console.log = originalLog;
|
console.log = originalLog;
|
||||||
@@ -471,7 +471,7 @@ describe('renderCursorStatus', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('renderCursorHelp', () => {
|
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 originalLog = console.log;
|
||||||
const logs: string[] = [];
|
const logs: string[] = [];
|
||||||
|
|
||||||
@@ -484,9 +484,16 @@ describe('renderCursorHelp', () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(logs.some((line) => line.includes('Usage: ccs cursor <subcommand>'))).toBe(true);
|
expect(logs.some((line) => line.includes('Usage: ccs cursor <subcommand>'))).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(
|
expect(logs.some((line) => line.includes('probe Run a live authenticated runtime probe'))).toBe(
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
logs.some((line) => line.includes('ccs cursor --auth'))
|
||||||
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
logs.some((line) => line.includes('ccs cursor [claude args]'))
|
logs.some((line) => line.includes('ccs cursor [claude args]'))
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -320,7 +320,7 @@ export function AddAccountDialog({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isGitLab && gitlabAuthMode === 'pat' && !gitlabPersonalAccessTokenTrimmed) {
|
if (isGitLab && gitlabAuthMode === 'pat' && !gitlabPersonalAccessTokenTrimmed) {
|
||||||
setLocalError('GitLab Personal Access Token is required for PAT login.');
|
setLocalError(t('addAccountDialog.gitlabPatRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wasAuthenticatingRef.current = true;
|
wasAuthenticatingRef.current = true;
|
||||||
@@ -538,7 +538,7 @@ export function AddAccountDialog({
|
|||||||
{isGitLab && !showAuthUI && (
|
{isGitLab && !showAuthUI && (
|
||||||
<div className="space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4">
|
<div className="space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="gitlab-auth-mode">GitLab auth method</Label>
|
<Label htmlFor="gitlab-auth-mode">{t('addAccountDialog.gitlabAuthMethod')}</Label>
|
||||||
<Select
|
<Select
|
||||||
value={gitlabAuthMode}
|
value={gitlabAuthMode}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -547,20 +547,20 @@ export function AddAccountDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="gitlab-auth-mode">
|
<SelectTrigger id="gitlab-auth-mode">
|
||||||
<SelectValue placeholder="Select GitLab auth method" />
|
<SelectValue placeholder={t('addAccountDialog.selectGitlabAuthMethod')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="oauth">Browser OAuth</SelectItem>
|
<SelectItem value="oauth">{t('addAccountDialog.gitlabAuthOAuth')}</SelectItem>
|
||||||
<SelectItem value="pat">Personal Access Token</SelectItem>
|
<SelectItem value="pat">{t('addAccountDialog.gitlabAuthPat')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Use Browser OAuth for gitlab.com, or PAT for self-hosted and admin-managed setups.
|
{t('addAccountDialog.gitlabAuthHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="gitlab-base-url">GitLab URL</Label>
|
<Label htmlFor="gitlab-base-url">{t('addAccountDialog.gitlabUrl')}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="gitlab-base-url"
|
id="gitlab-base-url"
|
||||||
value={gitlabBaseUrl}
|
value={gitlabBaseUrl}
|
||||||
@@ -568,17 +568,17 @@ export function AddAccountDialog({
|
|||||||
setGitlabBaseUrl(e.target.value);
|
setGitlabBaseUrl(e.target.value);
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
}}
|
}}
|
||||||
placeholder="https://gitlab.com"
|
placeholder={t('addAccountDialog.gitlabUrlPlaceholder')}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Optional. Leave blank for gitlab.com, or set your self-hosted GitLab base URL.
|
{t('addAccountDialog.gitlabUrlHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{gitlabAuthMode === 'pat' && (
|
{gitlabAuthMode === 'pat' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="gitlab-pat">Personal Access Token</Label>
|
<Label htmlFor="gitlab-pat">{t('addAccountDialog.gitlabPat')}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="gitlab-pat"
|
id="gitlab-pat"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -587,11 +587,11 @@ export function AddAccountDialog({
|
|||||||
setGitlabPersonalAccessToken(e.target.value);
|
setGitlabPersonalAccessToken(e.target.value);
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
}}
|
}}
|
||||||
placeholder="glpat-..."
|
placeholder={t('addAccountDialog.gitlabPatPlaceholder')}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Token must include at least <span className="font-mono">api</span> and{' '}
|
{t('addAccountDialog.gitlabPatHint')} <span className="font-mono">api</span> and{' '}
|
||||||
<span className="font-mono">read_user</span> scopes.
|
<span className="font-mono">read_user</span> scopes.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '/copilot', icon: Github, label: t('nav.githubCopilot') },
|
{ path: '/copilot', icon: Github, label: t('nav.githubCopilot') },
|
||||||
{ path: '/cursor', iconSrc: '/assets/sidebar/cursor.svg', label: t('nav.cursorIde') },
|
|
||||||
{
|
{
|
||||||
path: '/accounts',
|
path: '/accounts',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
@@ -124,6 +123,12 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] {
|
|||||||
{ path: '/droid', icon: TerminalSquare, label: t('nav.factoryDroid') },
|
{ 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'),
|
title: t('nav.system'),
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
+128
-34
@@ -1,6 +1,6 @@
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
import { getInitialLocale } from '@/lib/locales';
|
import { getInitialLocale } from './locales';
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en: {
|
en: {
|
||||||
@@ -30,6 +30,7 @@ const resources = {
|
|||||||
allAccounts: 'All Accounts',
|
allAccounts: 'All Accounts',
|
||||||
sharedData: 'Shared Data',
|
sharedData: 'Shared Data',
|
||||||
compatibleClis: 'Compatible',
|
compatibleClis: 'Compatible',
|
||||||
|
deprecated: 'Deprecated',
|
||||||
factoryDroid: 'Factory Droid',
|
factoryDroid: 'Factory Droid',
|
||||||
system: 'System',
|
system: 'System',
|
||||||
health: 'Health',
|
health: 'Health',
|
||||||
@@ -687,6 +688,20 @@ const resources = {
|
|||||||
'Power user mode is unavailable. Complete the required provider safety step and retry.',
|
'Power user mode is unavailable. Complete the required provider safety step and retry.',
|
||||||
authMethod: 'Auth Method',
|
authMethod: 'Auth Method',
|
||||||
selectKiroAuthMethod: 'Select Kiro 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)',
|
nicknameRequired: 'Nickname (required)',
|
||||||
nicknameOptional: 'Nickname (optional)',
|
nicknameOptional: 'Nickname (optional)',
|
||||||
nicknamePlaceholder: 'e.g., work, personal',
|
nicknamePlaceholder: 'e.g., work, personal',
|
||||||
@@ -1257,11 +1272,17 @@ const resources = {
|
|||||||
cursorPage: {
|
cursorPage: {
|
||||||
title: 'Cursor',
|
title: 'Cursor',
|
||||||
beta: 'Beta',
|
beta: 'Beta',
|
||||||
subtitle: 'Dedicated Cursor integration controls',
|
deprecated: 'Deprecated',
|
||||||
unofficialTitle: 'Unofficial API - Use at Your Own Risk',
|
subtitle: 'Deprecated local bridge for legacy Cursor setups',
|
||||||
unofficialItem1: 'Reverse-engineered integration may break anytime',
|
unofficialTitle: 'Deprecated legacy bridge - prefer CLIProxy-backed Cursor',
|
||||||
unofficialItem2: 'Abuse or excessive usage may risk account restrictions',
|
unofficialItem1: 'Supported path: CLIProxy-backed Cursor auth and account management',
|
||||||
unofficialItem3: 'No warranty, no responsibility from CCS',
|
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',
|
integration: 'Integration',
|
||||||
authentication: 'Authentication',
|
authentication: 'Authentication',
|
||||||
daemon: 'Daemon',
|
daemon: 'Daemon',
|
||||||
@@ -1272,11 +1293,11 @@ const resources = {
|
|||||||
notConnected: 'Not connected',
|
notConnected: 'Not connected',
|
||||||
running: 'Running',
|
running: 'Running',
|
||||||
stopped: 'Stopped',
|
stopped: 'Stopped',
|
||||||
actions: 'Actions',
|
actions: 'Legacy Actions',
|
||||||
disableIntegration: 'Disable Integration',
|
disableIntegration: 'Disable Integration',
|
||||||
enableIntegration: 'Enable Integration',
|
enableIntegration: 'Enable Integration',
|
||||||
autoDetectAuth: 'Auto-detect Auth',
|
autoDetectAuth: 'Legacy IDE Auto-detect',
|
||||||
manualAuthImport: 'Manual Auth Import',
|
manualAuthImport: 'Legacy Manual Import',
|
||||||
stopDaemon: 'Stop Daemon',
|
stopDaemon: 'Stop Daemon',
|
||||||
startDaemon: 'Start Daemon',
|
startDaemon: 'Start Daemon',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
@@ -2206,6 +2227,9 @@ const resources = {
|
|||||||
profileExportDownloaded: 'Profile export downloaded',
|
profileExportDownloaded: 'Profile export downloaded',
|
||||||
profileImportFailed: 'Failed to import profile bundle',
|
profileImportFailed: 'Failed to import profile bundle',
|
||||||
},
|
},
|
||||||
|
providerConfig: {
|
||||||
|
defaultDeviceCodeInstruction: 'Complete the authorization in your browser.',
|
||||||
|
},
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Domain 7: Profiles / Settings / Pages
|
// Domain 7: Profiles / Settings / Pages
|
||||||
@@ -2475,6 +2499,7 @@ const resources = {
|
|||||||
allAccounts: '全部账号',
|
allAccounts: '全部账号',
|
||||||
sharedData: '共享数据',
|
sharedData: '共享数据',
|
||||||
compatibleClis: '兼容',
|
compatibleClis: '兼容',
|
||||||
|
deprecated: '已弃用',
|
||||||
factoryDroid: 'Factory Droid',
|
factoryDroid: 'Factory Droid',
|
||||||
system: '系统',
|
system: '系统',
|
||||||
health: '健康',
|
health: '健康',
|
||||||
@@ -3079,6 +3104,18 @@ const resources = {
|
|||||||
powerUserUnavailableRetry: '高级用户模式不可用。请完成当前提供商要求的安全步骤后重试。',
|
powerUserUnavailableRetry: '高级用户模式不可用。请完成当前提供商要求的安全步骤后重试。',
|
||||||
authMethod: '认证方式',
|
authMethod: '认证方式',
|
||||||
selectKiroAuthMethod: '选择 Kiro 认证方式',
|
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: '昵称(必填)',
|
nicknameRequired: '昵称(必填)',
|
||||||
nicknameOptional: '昵称(选填)',
|
nicknameOptional: '昵称(选填)',
|
||||||
nicknamePlaceholder: '例如:工作、个人',
|
nicknamePlaceholder: '例如:工作、个人',
|
||||||
@@ -3628,11 +3665,17 @@ const resources = {
|
|||||||
cursorPage: {
|
cursorPage: {
|
||||||
title: 'Cursor',
|
title: 'Cursor',
|
||||||
beta: 'Beta',
|
beta: 'Beta',
|
||||||
subtitle: 'Cursor 集成专用控制面板',
|
deprecated: '已弃用',
|
||||||
unofficialTitle: '非官方 API - 风险自负',
|
subtitle: '面向旧版 Cursor 使用场景的已弃用本地桥接',
|
||||||
unofficialItem1: '逆向集成可能随时失效',
|
unofficialTitle: '旧版桥接已弃用,优先使用 CLIProxy 支持的 Cursor',
|
||||||
unofficialItem2: '滥用或过量使用可能导致账号受限',
|
unofficialItem1: '推荐路径:使用 CLIProxy 管理 Cursor 认证与账号',
|
||||||
unofficialItem3: 'CCS 不提供担保,也不承担责任',
|
unofficialItem2: '这个逆向本地桥接仅为仍在使用旧流程的用户保留',
|
||||||
|
unofficialItem3: 'CCS 不为这个旧路径提供担保,也不承担责任',
|
||||||
|
supportedPathTitle: '推荐路径',
|
||||||
|
supportedPathDesc:
|
||||||
|
'新的配置请使用 CLIProxy 支持的 Cursor 认证与账号管理。只有在你仍依赖旧的本地守护进程流程时,才继续使用下方旧版桥接。',
|
||||||
|
startCliproxyAuth: '启动 CLIProxy Cursor 认证',
|
||||||
|
openCliproxyCursor: '打开 CLIProxy Cursor',
|
||||||
integration: '集成',
|
integration: '集成',
|
||||||
authentication: '认证',
|
authentication: '认证',
|
||||||
daemon: '守护进程',
|
daemon: '守护进程',
|
||||||
@@ -3643,11 +3686,11 @@ const resources = {
|
|||||||
notConnected: '未连接',
|
notConnected: '未连接',
|
||||||
running: '运行中',
|
running: '运行中',
|
||||||
stopped: '已停止',
|
stopped: '已停止',
|
||||||
actions: '操作',
|
actions: '旧版操作',
|
||||||
disableIntegration: '禁用集成',
|
disableIntegration: '禁用集成',
|
||||||
enableIntegration: '启用集成',
|
enableIntegration: '启用集成',
|
||||||
autoDetectAuth: '自动检测认证',
|
autoDetectAuth: '旧版 IDE 自动检测',
|
||||||
manualAuthImport: '手动导入认证',
|
manualAuthImport: '旧版手动导入',
|
||||||
stopDaemon: '停止守护进程',
|
stopDaemon: '停止守护进程',
|
||||||
startDaemon: '启动守护进程',
|
startDaemon: '启动守护进程',
|
||||||
port: '端口',
|
port: '端口',
|
||||||
@@ -4543,6 +4586,9 @@ const resources = {
|
|||||||
profileExportDownloaded: '配置导出已下载',
|
profileExportDownloaded: '配置导出已下载',
|
||||||
profileImportFailed: '导入配置包失败',
|
profileImportFailed: '导入配置包失败',
|
||||||
},
|
},
|
||||||
|
providerConfig: {
|
||||||
|
defaultDeviceCodeInstruction: '请在浏览器中完成授权。',
|
||||||
|
},
|
||||||
profileEditorSections: {
|
profileEditorSections: {
|
||||||
imageAnalysis: '图片分析',
|
imageAnalysis: '图片分析',
|
||||||
loadingImageSettings: '加载图片设置中...',
|
loadingImageSettings: '加载图片设置中...',
|
||||||
@@ -4805,6 +4851,7 @@ const resources = {
|
|||||||
allAccounts: 'Tất cả tài khoản',
|
allAccounts: 'Tất cả tài khoản',
|
||||||
sharedData: 'Dữ liệu dùng chung',
|
sharedData: 'Dữ liệu dùng chung',
|
||||||
compatibleClis: 'Tương thích',
|
compatibleClis: 'Tương thích',
|
||||||
|
deprecated: 'Đã ngừng ưu tiên',
|
||||||
factoryDroid: 'Factory Droid',
|
factoryDroid: 'Factory Droid',
|
||||||
system: 'Hệ thống',
|
system: 'Hệ thống',
|
||||||
health: 'Sức khỏe',
|
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.',
|
'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',
|
authMethod: 'Phương thức xác thực',
|
||||||
selectKiroAuthMethod: 'Chọn phương thức xác thực Kiro',
|
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)',
|
nicknameRequired: 'Biệt danh (bắt buộc)',
|
||||||
nicknameOptional: 'Biệt danh (tùy chọn)',
|
nicknameOptional: 'Biệt danh (tùy chọn)',
|
||||||
nicknamePlaceholder: 'ví dụ: công việc, cá nhân',
|
nicknamePlaceholder: 'ví dụ: công việc, cá nhân',
|
||||||
@@ -6051,11 +6112,17 @@ const resources = {
|
|||||||
cursorPage: {
|
cursorPage: {
|
||||||
title: 'Cursor',
|
title: 'Cursor',
|
||||||
beta: 'Beta',
|
beta: 'Beta',
|
||||||
subtitle: 'Bảng điều khiển tích hợp Cursor',
|
deprecated: 'Đã ngừng ưu tiên',
|
||||||
unofficialTitle: 'API không chính thức - Bạn phải tự chịu rủi ro khi sử dụng',
|
subtitle: 'Cầu nối cục bộ đã bị ngừng ưu tiên cho các thiết lập Cursor cũ',
|
||||||
unofficialItem1: 'Tích hợp thiết kế ngược có thể bị hỏng bất cứ lúc nào',
|
unofficialTitle: 'Cầu nối cũ đã bị ngừng ưu tiên - hãy dùng Cursor qua CLIProxy',
|
||||||
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',
|
unofficialItem1: 'Đường dẫn được hỗ trợ: xác thực và quản lý tài khoản Cursor qua CLIProxy',
|
||||||
unofficialItem3: 'Không bảo hành, không chịu trách nhiệm từ CCS',
|
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',
|
integration: 'Tích hợp',
|
||||||
authentication: 'Xác thực',
|
authentication: 'Xác thực',
|
||||||
daemon: 'Daemon',
|
daemon: 'Daemon',
|
||||||
@@ -6066,11 +6133,11 @@ const resources = {
|
|||||||
notConnected: 'Chưa kết nối',
|
notConnected: 'Chưa kết nối',
|
||||||
running: 'Đang chạy',
|
running: 'Đang chạy',
|
||||||
stopped: 'Đã dừng',
|
stopped: 'Đã dừng',
|
||||||
actions: 'Hành động',
|
actions: 'Hành động cũ',
|
||||||
disableIntegration: 'Vô hiệu hóa tích hợp',
|
disableIntegration: 'Vô hiệu hóa tích hợp',
|
||||||
enableIntegration: 'Kích hoạt tích hợp',
|
enableIntegration: 'Kích hoạt tích hợp',
|
||||||
autoDetectAuth: 'Tự động phát hiện xác thực',
|
autoDetectAuth: 'Tự dò IDE cũ',
|
||||||
manualAuthImport: 'Nhập xác thực thủ công',
|
manualAuthImport: 'Nhập thủ công kiểu cũ',
|
||||||
stopDaemon: 'Dừng Daemon',
|
stopDaemon: 'Dừng Daemon',
|
||||||
startDaemon: 'Khởi động Daemon',
|
startDaemon: 'Khởi động Daemon',
|
||||||
port: 'Cổng',
|
port: 'Cổng',
|
||||||
@@ -6978,6 +7045,9 @@ const resources = {
|
|||||||
profileExportDownloaded: 'Đã tải xuống xuất hồ sơ',
|
profileExportDownloaded: 'Đã tải xuống xuất hồ sơ',
|
||||||
profileImportFailed: 'Không nhập được gói 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: {
|
profileEditorSections: {
|
||||||
imageAnalysis: 'Phân tích hình ảnh',
|
imageAnalysis: 'Phân tích hình ảnh',
|
||||||
loadingImageSettings: 'Đang tải cài đặt hình ảnh...',
|
loadingImageSettings: 'Đang tải cài đặt hình ảnh...',
|
||||||
@@ -7243,6 +7313,7 @@ const resources = {
|
|||||||
allAccounts: 'すべてのアカウント',
|
allAccounts: 'すべてのアカウント',
|
||||||
sharedData: '共有データ',
|
sharedData: '共有データ',
|
||||||
compatibleClis: '互換',
|
compatibleClis: '互換',
|
||||||
|
deprecated: '非推奨',
|
||||||
factoryDroid: 'Factory Droid',
|
factoryDroid: 'Factory Droid',
|
||||||
system: 'システム',
|
system: 'システム',
|
||||||
health: 'ヘルス',
|
health: 'ヘルス',
|
||||||
@@ -7911,6 +7982,20 @@ const resources = {
|
|||||||
'パワーユーザーモードは利用できません。必要なプロバイダーの安全確認を完了してから再試行してください。',
|
'パワーユーザーモードは利用できません。必要なプロバイダーの安全確認を完了してから再試行してください。',
|
||||||
authMethod: '認証方法',
|
authMethod: '認証方法',
|
||||||
selectKiroAuthMethod: 'Kiro の認証方法を選択',
|
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: 'ニックネーム(必須)',
|
nicknameRequired: 'ニックネーム(必須)',
|
||||||
nicknameOptional: 'ニックネーム(任意)',
|
nicknameOptional: 'ニックネーム(任意)',
|
||||||
nicknamePlaceholder: '例: work, personal',
|
nicknamePlaceholder: '例: work, personal',
|
||||||
@@ -8494,12 +8579,18 @@ const resources = {
|
|||||||
cursorPage: {
|
cursorPage: {
|
||||||
title: 'Cursor',
|
title: 'Cursor',
|
||||||
beta: 'ベータ',
|
beta: 'ベータ',
|
||||||
subtitle: 'Cursor 専用の連携設定',
|
deprecated: '非推奨',
|
||||||
unofficialTitle: '非公式API - 自己責任で利用',
|
subtitle: '旧 Cursor セットアップ向けの非推奨ローカルブリッジ',
|
||||||
unofficialItem1:
|
unofficialTitle: 'レガシーブリッジは非推奨です。CLIProxy 管理の Cursor を優先してください',
|
||||||
'リバースエンジニアリング連携のため、いつでも利用不能になる可能性があります',
|
unofficialItem1: '推奨パス: CLIProxy で Cursor の認証とアカウント管理を行う',
|
||||||
unofficialItem2: '不正利用や過度な利用により、アカウントが制限されるおそれがあります',
|
unofficialItem2:
|
||||||
unofficialItem3: 'CCSは保証も責任も負いません',
|
'このリバースエンジニアリングされたローカルブリッジは、既存セットアップ向けの互換用です',
|
||||||
|
unofficialItem3: 'このレガシーパスについて CCS は保証も責任も負いません',
|
||||||
|
supportedPathTitle: '推奨パス',
|
||||||
|
supportedPathDesc:
|
||||||
|
'新しいセットアップでは、CLIProxy 管理の Cursor 認証とアカウント管理を使ってください。旧来のローカルデーモンに依存している場合のみ、下のレガシーブリッジを使い続けてください。',
|
||||||
|
startCliproxyAuth: 'CLIProxy で Cursor 認証を開始',
|
||||||
|
openCliproxyCursor: 'CLIProxy の Cursor を開く',
|
||||||
integration: '連携',
|
integration: '連携',
|
||||||
authentication: '認証',
|
authentication: '認証',
|
||||||
daemon: 'デーモン',
|
daemon: 'デーモン',
|
||||||
@@ -8510,11 +8601,11 @@ const resources = {
|
|||||||
notConnected: '未接続',
|
notConnected: '未接続',
|
||||||
running: '稼働中',
|
running: '稼働中',
|
||||||
stopped: '停止中',
|
stopped: '停止中',
|
||||||
actions: '操作',
|
actions: 'レガシー操作',
|
||||||
disableIntegration: '連携を無効化',
|
disableIntegration: '連携を無効化',
|
||||||
enableIntegration: '連携を有効化',
|
enableIntegration: '連携を有効化',
|
||||||
autoDetectAuth: '認証を自動検出',
|
autoDetectAuth: '旧 IDE から自動検出',
|
||||||
manualAuthImport: '認証情報を手動インポート',
|
manualAuthImport: '旧方式で手動インポート',
|
||||||
stopDaemon: 'デーモンを停止',
|
stopDaemon: 'デーモンを停止',
|
||||||
startDaemon: 'デーモンを起動',
|
startDaemon: 'デーモンを起動',
|
||||||
port: 'ポート',
|
port: 'ポート',
|
||||||
@@ -9648,6 +9739,9 @@ const resources = {
|
|||||||
profileExportDownloaded: 'プロファイルエクスポートをダウンロードしました',
|
profileExportDownloaded: 'プロファイルエクスポートをダウンロードしました',
|
||||||
profileImportFailed: 'プロファイルバンドルのインポートに失敗しました',
|
profileImportFailed: 'プロファイルバンドルのインポートに失敗しました',
|
||||||
},
|
},
|
||||||
|
providerConfig: {
|
||||||
|
defaultDeviceCodeInstruction: 'ブラウザーで認証を完了してください。',
|
||||||
|
},
|
||||||
updatesSpotlight: {
|
updatesSpotlight: {
|
||||||
openUpdatesCenter: '更新センターを開く',
|
openUpdatesCenter: '更新センターを開く',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getProvidersByOAuthFlow,
|
getProvidersByOAuthFlow,
|
||||||
} from '../../../src/cliproxy/provider-capabilities';
|
} from '../../../src/cliproxy/provider-capabilities';
|
||||||
import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/cliproxy/ai-providers';
|
import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/cliproxy/ai-providers';
|
||||||
|
import i18n from './i18n';
|
||||||
|
|
||||||
// Monorepo contract: UI consumes provider capability constants directly from backend
|
// Monorepo contract: UI consumes provider capability constants directly from backend
|
||||||
// to enforce one source of truth and prevent provider drift across surfaces.
|
// to enforce one source of truth and prevent provider drift across surfaces.
|
||||||
@@ -253,9 +254,9 @@ const PROVIDER_NAMES: Record<string, string> = {
|
|||||||
export function getProviderDisplayName(provider: unknown): string {
|
export function getProviderDisplayName(provider: unknown): string {
|
||||||
const normalized = normalizeProviderInput(provider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
if (!normalized) {
|
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 */
|
/** Map provider to user-facing short description */
|
||||||
@@ -297,12 +298,12 @@ export function isDeviceCodeProvider(provider: unknown): boolean {
|
|||||||
export function getDeviceCodeProviderDisplayName(provider: unknown): string {
|
export function getDeviceCodeProviderDisplayName(provider: unknown): string {
|
||||||
const normalized = normalizeProviderInput(provider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return 'Unknown provider'; // TODO i18n: missing key
|
return i18n.t('toasts.providerUnknown', { provider: 'unknown' });
|
||||||
}
|
}
|
||||||
if (isValidProvider(normalized)) {
|
if (isValidProvider(normalized)) {
|
||||||
return DEVICE_CODE_PROVIDER_DISPLAY_NAMES[normalized] || getProviderDisplayName(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. */
|
/** Provider-specific helper text for device-code dialog. */
|
||||||
@@ -310,10 +311,11 @@ export function getDeviceCodeProviderInstruction(provider: unknown): string {
|
|||||||
const normalized = normalizeProviderInput(provider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
if (isValidProvider(normalized)) {
|
if (isValidProvider(normalized)) {
|
||||||
return (
|
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). */
|
/** Kiro auth methods exposed in CCS UI (aligned with CLIProxyAPIPlus support). */
|
||||||
|
|||||||
+34
-2
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useMemo, useState, type ElementType } from 'react';
|
import { useMemo, useState, type ElementType } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -289,6 +290,7 @@ function StatusItem({
|
|||||||
|
|
||||||
export function CursorPage() {
|
export function CursorPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
statusLoading,
|
statusLoading,
|
||||||
@@ -733,9 +735,9 @@ export function CursorPage() {
|
|||||||
<h1 className="font-semibold">{t('cursorPage.title')}</h1>
|
<h1 className="font-semibold">{t('cursorPage.title')}</h1>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-5 border-amber-500/60 bg-amber-500/10 px-1.5 text-[10px] font-semibold uppercase tracking-wide text-amber-800 dark:text-amber-300"
|
className="h-5 border-red-500/50 bg-red-500/10 px-1.5 text-[10px] font-semibold uppercase tracking-wide text-red-700 dark:text-red-300"
|
||||||
>
|
>
|
||||||
{t('cursorPage.beta')}
|
{t('cursorPage.deprecated')}
|
||||||
</Badge>
|
</Badge>
|
||||||
{integrationBadge}
|
{integrationBadge}
|
||||||
</div>
|
</div>
|
||||||
@@ -770,6 +772,36 @@ export function CursorPage() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border border-border/70 bg-background/90 p-3 space-y-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
|
{t('cursorPage.supportedPathTitle')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t('cursorPage.supportedPathDesc')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => navigate('/cliproxy?provider=cursor&action=auth')}
|
||||||
|
>
|
||||||
|
<Key className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
{t('cursorPage.startCliproxyAuth')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => navigate('/cliproxy?provider=cursor')}
|
||||||
|
>
|
||||||
|
<Zap className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
{t('cursorPage.openCliproxyCursor')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={ShieldCheck}
|
icon={ShieldCheck}
|
||||||
|
|||||||
Reference in New Issue
Block a user