mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
feat(cliproxy): integrate missing provider support
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/codebuddy",
|
||||
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
|
||||
"ANTHROPIC_MODEL": "auto",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-5.1",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-k2.5",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v3-2-volc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/cursor",
|
||||
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
|
||||
"ANTHROPIC_MODEL": "composer-2",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-4-sonnet",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-4-sonnet",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "cursor-small"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/gitlab",
|
||||
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
|
||||
"ANTHROPIC_MODEL": "gitlab-duo",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "duo-chat-opus-4-6",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "duo-chat-sonnet-4-6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "duo-chat-haiku-4-5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/kilo",
|
||||
"ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed",
|
||||
"ANTHROPIC_MODEL": "kilo/auto",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "kilo/auto",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "kilo/auto",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "kilo/auto"
|
||||
}
|
||||
}
|
||||
@@ -253,9 +253,8 @@ class ProfileDetector {
|
||||
* Detect profile type and return routing information
|
||||
*
|
||||
* Priority order:
|
||||
* 0. Hardcoded CLIProxy profiles (gemini, codex, agy, qwen)
|
||||
* 0.5. Copilot profile (if enabled in config)
|
||||
* 0.75. Cursor profile (if enabled in config)
|
||||
* 0. Hardcoded special runtime profiles (copilot, cursor)
|
||||
* 0.5. Hardcoded CLIProxy profiles (gemini, codex, agy, qwen, ...)
|
||||
* 1. Unified config profiles (if config.yaml exists or CCS_UNIFIED_CONFIG=1)
|
||||
* 2. User-defined CLIProxy variants (config.cliproxy section) [legacy]
|
||||
* 3. Settings-based profiles (config.profiles section) [legacy]
|
||||
@@ -267,16 +266,7 @@ class ProfileDetector {
|
||||
return this.resolveDefaultProfile();
|
||||
}
|
||||
|
||||
// Priority 0: Check CLIProxy profiles (gemini, codex, agy, qwen) - OAuth-based, zero config
|
||||
if (isCLIProxyProvider(profileName)) {
|
||||
return {
|
||||
type: 'cliproxy',
|
||||
name: profileName,
|
||||
provider: profileName,
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 0.5: Check Copilot profile - GitHub Copilot subscription via copilot-api
|
||||
// Priority 0: Check Copilot profile - GitHub Copilot subscription via copilot-api
|
||||
if (profileName === 'copilot') {
|
||||
const unifiedConfig = this.readUnifiedConfig();
|
||||
const copilotConfig = unifiedConfig?.copilot;
|
||||
@@ -306,7 +296,10 @@ class ProfileDetector {
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 0.75: Check Cursor profile - local Cursor daemon runtime
|
||||
// Priority 0.25: Check Cursor profile - local Cursor daemon runtime.
|
||||
// This keeps bare `ccs cursor` and `ccs cursor <subcommand>` bound to the
|
||||
// existing runtime/admin surface even though CLIProxy also exposes a
|
||||
// distinct provider named "cursor".
|
||||
if (profileName === 'cursor') {
|
||||
const cursorConfig = getCursorConfig();
|
||||
|
||||
@@ -334,6 +327,15 @@ class ProfileDetector {
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 0.5: Check CLIProxy profiles (gemini, codex, agy, qwen, ...)
|
||||
if (isCLIProxyProvider(profileName)) {
|
||||
return {
|
||||
type: 'cliproxy',
|
||||
name: profileName,
|
||||
provider: profileName,
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 1: Try unified config if available
|
||||
const unifiedConfig = this.readUnifiedConfig();
|
||||
if (unifiedConfig) {
|
||||
|
||||
+20
-2
@@ -77,6 +77,7 @@ import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils
|
||||
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
|
||||
import { createLogger } from './services/logging';
|
||||
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
|
||||
import type { ProfileDetectionResult } from './auth/profile-detector';
|
||||
|
||||
// Import target adapter system
|
||||
import {
|
||||
@@ -122,6 +123,7 @@ interface RuntimeReasoningResolution {
|
||||
sourceDisplay: string | undefined;
|
||||
}
|
||||
|
||||
const CURSOR_CLIPROXY_SHORTCUT_FLAGS = new Set(['--auth', '--logout', '--config', '--accounts']);
|
||||
const CODEX_RUNTIME_REASONING_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
|
||||
const CODEX_NATIVE_PASSTHROUGH_FLAGS = new Set(['--help', '-h', '--version', '-v']);
|
||||
|
||||
@@ -147,6 +149,13 @@ function detectProfile(args: string[]): DetectedProfile {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUseCursorCliproxyShortcut(args: string[]): boolean {
|
||||
return (
|
||||
args[0] === 'cursor' &&
|
||||
args.some((arg, index) => index > 0 && CURSOR_CLIPROXY_SHORTCUT_FLAGS.has(arg))
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRuntimeReasoningFlags(
|
||||
args: string[],
|
||||
envThinkingValue: string | undefined
|
||||
@@ -537,8 +546,17 @@ async function main(): Promise<void> {
|
||||
try {
|
||||
// Detect profile (strip --target flags before profile detection)
|
||||
const cleanArgs = stripTargetFlag(args);
|
||||
const { profile, remainingArgs } = detectProfile(cleanArgs);
|
||||
const profileInfo = detector.detectProfileType(profile);
|
||||
const useCursorCliproxyShortcut = shouldUseCursorCliproxyShortcut(cleanArgs);
|
||||
const { profile, remainingArgs } = useCursorCliproxyShortcut
|
||||
? { profile: 'cursor', remainingArgs: cleanArgs.slice(1) }
|
||||
: detectProfile(cleanArgs);
|
||||
const profileInfo: ProfileDetectionResult = useCursorCliproxyShortcut
|
||||
? {
|
||||
type: 'cliproxy',
|
||||
name: 'cursor',
|
||||
provider: 'cursor',
|
||||
}
|
||||
: detector.detectProfileType(profile);
|
||||
let resolvedTarget: ReturnType<typeof resolveTargetType>;
|
||||
try {
|
||||
resolvedTarget = resolveTargetType(
|
||||
|
||||
@@ -155,6 +155,10 @@ export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google'
|
||||
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
|
||||
* - GHCP: Device Code Flow (polling-based, NO callback port needed)
|
||||
* - Kimi: Device Code Flow (polling-based, NO callback port needed)
|
||||
* - Cursor: Device-style browser polling (NO callback port needed)
|
||||
* - GitLab: Authorization Code Flow with callback server on port 17171
|
||||
* - CodeBuddy: Device-style browser polling (NO callback port needed)
|
||||
* - Kilo: Device Code Flow (polling-based, NO callback port needed)
|
||||
*/
|
||||
export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> =
|
||||
CLIPROXY_PROVIDER_IDS.reduce(
|
||||
@@ -274,6 +278,34 @@ export const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
|
||||
scopes: ['api'],
|
||||
authFlag: '--kimi-login',
|
||||
},
|
||||
cursor: {
|
||||
provider: 'cursor',
|
||||
displayName: 'Cursor',
|
||||
authUrl: 'https://cursor.com/loginDeepControl',
|
||||
scopes: [],
|
||||
authFlag: '--cursor-login',
|
||||
},
|
||||
gitlab: {
|
||||
provider: 'gitlab',
|
||||
displayName: 'GitLab Duo',
|
||||
authUrl: 'https://gitlab.com/oauth/authorize',
|
||||
scopes: ['api', 'read_user'],
|
||||
authFlag: '--gitlab-login',
|
||||
},
|
||||
codebuddy: {
|
||||
provider: 'codebuddy',
|
||||
displayName: 'CodeBuddy (Tencent)',
|
||||
authUrl: 'https://copilot.tencent.com/v2/plugin/auth/state',
|
||||
scopes: [],
|
||||
authFlag: '--codebuddy-login',
|
||||
},
|
||||
kilo: {
|
||||
provider: 'kilo',
|
||||
displayName: 'Kilo AI',
|
||||
authUrl: 'https://api.kilo.ai/api/device-auth/codes',
|
||||
scopes: [],
|
||||
authFlag: '--kilo-login',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -373,6 +405,12 @@ export interface OAuthOptions {
|
||||
noIncognito?: boolean;
|
||||
/** If true, skip OAuth and import token from Kiro IDE directly (Kiro only) */
|
||||
import?: boolean;
|
||||
/** GitLab auth mode override. */
|
||||
gitlabAuthMode?: 'oauth' | 'pat';
|
||||
/** GitLab self-hosted base URL override. */
|
||||
gitlabBaseUrl?: string;
|
||||
/** GitLab personal access token for PAT login. */
|
||||
gitlabPersonalAccessToken?: string;
|
||||
/** Enable paste-callback mode: show auth URL and prompt for callback paste */
|
||||
pasteCallback?: boolean;
|
||||
/** If true, use port-forwarding mode (skip interactive prompt in headless) */
|
||||
|
||||
@@ -83,9 +83,12 @@ const POLLED_AUTH_LOCAL_TOKEN_GRACE_MS = 15 * 1000;
|
||||
export async function requestPasteCallbackStart(
|
||||
provider: CLIProxyProvider,
|
||||
target: ProxyTarget,
|
||||
options?: { kiroMethod?: OAuthOptions['kiroMethod'] }
|
||||
options?: {
|
||||
kiroMethod?: OAuthOptions['kiroMethod'];
|
||||
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||
}
|
||||
): Promise<PasteCallbackStartData> {
|
||||
const startPath = getPasteCallbackStartPath(provider, {
|
||||
let startPath = getPasteCallbackStartPath(provider, {
|
||||
kiroMethod: options?.kiroMethod,
|
||||
});
|
||||
if (!startPath) {
|
||||
@@ -93,6 +96,9 @@ export async function requestPasteCallbackStart(
|
||||
`Paste-callback start is not available for ${provider} with the selected method`
|
||||
);
|
||||
}
|
||||
if (provider === 'gitlab' && options?.gitlabBaseUrl?.trim()) {
|
||||
startPath += `&base_url=${encodeURIComponent(options.gitlabBaseUrl.trim())}`;
|
||||
}
|
||||
const response = await fetch(buildProxyUrl(target, startPath), {
|
||||
headers: buildManagementHeaders(target),
|
||||
});
|
||||
@@ -142,6 +148,27 @@ function parseAuthUrlState(url: string | null | undefined): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGitLabBaseUrl(baseUrl: string | undefined): string | undefined {
|
||||
const normalized = baseUrl?.trim();
|
||||
return normalized ? normalized : undefined;
|
||||
}
|
||||
|
||||
async function promptGitLabPersonalAccessToken(): Promise<string | null> {
|
||||
const readline = await import('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise<string | null>((resolve) => {
|
||||
rl.question('GitLab Personal Access Token: ', (answer) => {
|
||||
rl.close();
|
||||
const token = answer.trim();
|
||||
resolve(token.length > 0 ? token : null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function findNewTokenSnapshotForManualAuth(
|
||||
provider: CLIProxyProvider,
|
||||
tokenDir: string,
|
||||
@@ -458,7 +485,10 @@ async function handlePasteCallbackMode(
|
||||
tokenDir: string,
|
||||
nickname?: string,
|
||||
expectedAccountId?: string,
|
||||
options?: { kiroMethod?: OAuthOptions['kiroMethod'] }
|
||||
options?: {
|
||||
kiroMethod?: OAuthOptions['kiroMethod'];
|
||||
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||
}
|
||||
): Promise<AccountInfo | null> {
|
||||
// Resolve CLIProxyAPI target (local or remote based on config)
|
||||
const target = getProxyTarget();
|
||||
@@ -647,6 +677,97 @@ async function handlePasteCallbackMode(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGitLabPatLogin(
|
||||
provider: CLIProxyProvider,
|
||||
oauthConfig: ProviderOAuthConfig,
|
||||
verbose: boolean,
|
||||
tokenDir: string,
|
||||
nickname?: string,
|
||||
expectedAccountId?: string,
|
||||
options?: {
|
||||
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||
gitlabPersonalAccessToken?: OAuthOptions['gitlabPersonalAccessToken'];
|
||||
}
|
||||
): Promise<AccountInfo | null> {
|
||||
const target = getProxyTarget();
|
||||
const baseUrl = normalizeGitLabBaseUrl(options?.gitlabBaseUrl);
|
||||
const knownTokenFiles = listProviderTokenSnapshots(provider, tokenDir);
|
||||
const suppliedToken = options?.gitlabPersonalAccessToken?.trim();
|
||||
const personalAccessToken =
|
||||
suppliedToken || process.env['GITLAB_PERSONAL_ACCESS_TOKEN']?.trim() || undefined;
|
||||
|
||||
let token = personalAccessToken;
|
||||
if (!token) {
|
||||
console.log('');
|
||||
console.log(info(`Starting ${oauthConfig.displayName} PAT login...`));
|
||||
console.log('Paste a Personal Access Token with api and read_user scopes.');
|
||||
token = (await promptGitLabPersonalAccessToken()) || undefined;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.log(info('Cancelled'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(buildProxyUrl(target, '/v0/management/gitlab-auth-url'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...buildManagementHeaders(target),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(baseUrl ? { base_url: baseUrl } : {}),
|
||||
personal_access_token: token,
|
||||
}),
|
||||
});
|
||||
|
||||
const responseBody = (await response.text()).trim();
|
||||
let payload: Record<string, unknown> = {};
|
||||
if (responseBody) {
|
||||
try {
|
||||
payload = JSON.parse(responseBody) as Record<string, unknown>;
|
||||
} catch {
|
||||
payload = { error: responseBody };
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok || payload.status !== 'ok') {
|
||||
const errorMessage =
|
||||
(typeof payload.error === 'string' && payload.error) ||
|
||||
responseBody ||
|
||||
`GitLab PAT login failed with status ${response.status}`;
|
||||
console.log(fail(errorMessage));
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenSnapshot = findNewTokenSnapshotForAuthAttempt(
|
||||
provider,
|
||||
tokenDir,
|
||||
knownTokenFiles,
|
||||
expectedAccountId
|
||||
);
|
||||
if (!tokenSnapshot) {
|
||||
console.log(fail('GitLab PAT login completed, but CCS could not find the saved token file.'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = registerAccountFromToken(
|
||||
provider,
|
||||
tokenDir,
|
||||
nickname,
|
||||
verbose,
|
||||
expectedAccountId || tokenSnapshot.file
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
console.log(fail('Authenticated GitLab token could not be registered as a CCS account.'));
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(ok('Authentication successful!'));
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger OAuth flow for provider
|
||||
* Auto-detects headless environment and uses --no-browser flag accordingly
|
||||
@@ -666,6 +787,10 @@ export async function triggerOAuth(
|
||||
provider === 'kiro' ? normalizeKiroAuthMethod(options.kiroMethod) : DEFAULT_KIRO_AUTH_METHOD;
|
||||
const resolvedKiroIDCFlow =
|
||||
provider === 'kiro' ? normalizeKiroIDCFlow(options.kiroIDCFlow) : DEFAULT_KIRO_IDC_FLOW;
|
||||
const resolvedGitLabAuthMode =
|
||||
provider === 'gitlab' && options.gitlabAuthMode === 'pat' ? 'pat' : 'oauth';
|
||||
const resolvedGitLabBaseUrl =
|
||||
provider === 'gitlab' ? normalizeGitLabBaseUrl(options.gitlabBaseUrl) : undefined;
|
||||
|
||||
if (provider === 'agy') {
|
||||
if (fromUI && !acceptAgyRisk) {
|
||||
@@ -744,6 +869,14 @@ export async function triggerOAuth(
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && resolvedGitLabBaseUrl && !selectedPasteCallback) {
|
||||
selectedPasteCallback = true;
|
||||
console.log('');
|
||||
console.log(
|
||||
info('GitLab custom base URL selected. Switching to paste-callback mode for OAuth.')
|
||||
);
|
||||
}
|
||||
|
||||
const useSelectedKiroLocalPasteCallback =
|
||||
selectedPasteCallback &&
|
||||
provider === 'kiro' &&
|
||||
@@ -765,6 +898,22 @@ export async function triggerOAuth(
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && resolvedGitLabAuthMode === 'pat') {
|
||||
const tokenDir = getProviderTokenDir(provider);
|
||||
return handleGitLabPatLogin(
|
||||
provider,
|
||||
oauthConfig,
|
||||
verbose,
|
||||
tokenDir,
|
||||
nickname,
|
||||
existingNameMatch?.id,
|
||||
{
|
||||
gitlabBaseUrl: resolvedGitLabBaseUrl,
|
||||
gitlabPersonalAccessToken: options.gitlabPersonalAccessToken,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedPasteCallback && !useSelectedKiroDirectCliFlow) {
|
||||
const tokenDir = getProviderTokenDir(provider);
|
||||
return handlePasteCallbackMode(
|
||||
@@ -774,7 +923,10 @@ export async function triggerOAuth(
|
||||
tokenDir,
|
||||
nickname,
|
||||
existingNameMatch?.id,
|
||||
{ kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined }
|
||||
{
|
||||
kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined,
|
||||
gitlabBaseUrl: provider === 'gitlab' ? resolvedGitLabBaseUrl : undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -353,6 +353,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 acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
|
||||
|
||||
const incognitoFlag = argsWithoutProxy.includes('--incognito');
|
||||
@@ -444,6 +445,16 @@ export async function execClaudeWithCLIProxy(
|
||||
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
|
||||
}
|
||||
|
||||
let gitlabBaseUrl: string | undefined;
|
||||
const gitlabBaseUrlValue = readOptionValue(argsWithoutProxy, '--gitlab-url');
|
||||
if (gitlabBaseUrlValue.present && gitlabBaseUrlValue.value) {
|
||||
gitlabBaseUrl = gitlabBaseUrlValue.value.trim();
|
||||
} else if (gitlabBaseUrlValue.present) {
|
||||
console.error(fail('--gitlab-url requires a value'));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
|
||||
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
||||
process.exitCode = 1;
|
||||
@@ -491,6 +502,13 @@ export async function execClaudeWithCLIProxy(
|
||||
return;
|
||||
}
|
||||
|
||||
if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') {
|
||||
const flagName = gitlabTokenLogin ? '--token-login' : '--gitlab-url';
|
||||
console.error(fail(`${flagName} is only valid for ccs gitlab`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse --thinking / --effort flags (aliases; first occurrence wins)
|
||||
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
|
||||
if (thinkingParse.error) {
|
||||
@@ -730,6 +748,8 @@ export async function execClaudeWithCLIProxy(
|
||||
...(kiroIDCStartUrl && p === 'kiro' ? { kiroIDCStartUrl } : {}),
|
||||
...(kiroIDCRegion && p === 'kiro' ? { kiroIDCRegion } : {}),
|
||||
...(kiroIDCFlow && p === 'kiro' ? { kiroIDCFlow } : {}),
|
||||
...(gitlabTokenLogin && p === 'gitlab' ? { gitlabAuthMode: 'pat' as const } : {}),
|
||||
...(gitlabBaseUrl && p === 'gitlab' ? { gitlabBaseUrl } : {}),
|
||||
...(forceHeadless ? { headless: true } : {}),
|
||||
...(setNickname ? { nickname: setNickname } : {}),
|
||||
...(noIncognito ? { noIncognito: true } : {}),
|
||||
@@ -775,6 +795,8 @@ export async function execClaudeWithCLIProxy(
|
||||
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
|
||||
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
|
||||
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
|
||||
...(gitlabTokenLogin ? { gitlabAuthMode: 'pat' as const } : {}),
|
||||
...(gitlabBaseUrl ? { gitlabBaseUrl } : {}),
|
||||
...(forceHeadless ? { headless: true } : {}),
|
||||
...(setNickname ? { nickname: setNickname } : {}),
|
||||
...(noIncognito ? { noIncognito: true } : {}),
|
||||
|
||||
@@ -134,6 +134,54 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
tokenTypeValues: ['kimi'],
|
||||
aliases: ['moonshot'],
|
||||
},
|
||||
cursor: {
|
||||
displayName: 'Cursor',
|
||||
description: 'Cursor browser-authenticated provider',
|
||||
oauthFlow: 'device_code',
|
||||
callbackPort: null,
|
||||
callbackProviderName: 'cursor',
|
||||
authUrlProviderName: 'cursor',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authFilePrefixes: ['cursor.', 'cursor-'],
|
||||
tokenTypeValues: ['cursor'],
|
||||
aliases: [],
|
||||
},
|
||||
gitlab: {
|
||||
displayName: 'GitLab Duo',
|
||||
description: 'GitLab Duo with OAuth or PAT auth',
|
||||
oauthFlow: 'authorization_code',
|
||||
callbackPort: 17171,
|
||||
callbackProviderName: 'gitlab',
|
||||
authUrlProviderName: 'gitlab',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authFilePrefixes: ['gitlab-'],
|
||||
tokenTypeValues: ['gitlab'],
|
||||
aliases: ['gitlab-duo'],
|
||||
},
|
||||
codebuddy: {
|
||||
displayName: 'CodeBuddy (Tencent)',
|
||||
description: 'Tencent CodeBuddy AI assistant',
|
||||
oauthFlow: 'device_code',
|
||||
callbackPort: null,
|
||||
callbackProviderName: 'codebuddy',
|
||||
authUrlProviderName: 'codebuddy',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authFilePrefixes: ['codebuddy-'],
|
||||
tokenTypeValues: ['codebuddy'],
|
||||
aliases: ['tencent'],
|
||||
},
|
||||
kilo: {
|
||||
displayName: 'Kilo AI',
|
||||
description: 'Kilo AI coding assistant',
|
||||
oauthFlow: 'device_code',
|
||||
callbackPort: null,
|
||||
callbackProviderName: 'kilo',
|
||||
authUrlProviderName: 'kilo',
|
||||
refreshOwnership: 'unsupported',
|
||||
authFilePrefixes: ['kilo-'],
|
||||
tokenTypeValues: ['kilo'],
|
||||
aliases: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const CLIPROXY_PROVIDER_IDS = Object.freeze(
|
||||
|
||||
+19
-4
@@ -122,6 +122,10 @@ export interface DownloadResult {
|
||||
* - ghcp: GitHub Copilot via Device Code (OAuth through CLIProxyAPIPlus)
|
||||
* - claude: Claude (Anthropic) via OAuth
|
||||
* - kimi: Kimi (Moonshot AI) via Device Code OAuth
|
||||
* - cursor: Cursor via PKCE browser polling
|
||||
* - gitlab: GitLab Duo via OAuth or PAT
|
||||
* - codebuddy: Tencent CodeBuddy via browser polling
|
||||
* - kilo: Kilo AI via device flow
|
||||
*/
|
||||
export type CLIProxyProvider =
|
||||
| 'gemini'
|
||||
@@ -132,12 +136,16 @@ export type CLIProxyProvider =
|
||||
| 'kiro'
|
||||
| 'ghcp'
|
||||
| 'claude'
|
||||
| 'kimi';
|
||||
| 'kimi'
|
||||
| 'cursor'
|
||||
| 'gitlab'
|
||||
| 'codebuddy'
|
||||
| 'kilo';
|
||||
|
||||
/**
|
||||
* CLIProxy backend selection
|
||||
* - original: CLIProxyAPI (no Kiro/ghcp support)
|
||||
* - plus: CLIProxyAPIPlus (Kiro/ghcp support, default)
|
||||
* - original: CLIProxyAPI (legacy provider subset)
|
||||
* - plus: CLIProxyAPIPlus (expanded provider support, default)
|
||||
*/
|
||||
export type CLIProxyBackend = 'original' | 'plus';
|
||||
|
||||
@@ -149,7 +157,14 @@ export type CliproxyRoutingStrategy = 'round-robin' | 'fill-first';
|
||||
/**
|
||||
* Providers that require CLIProxyAPIPlus backend
|
||||
*/
|
||||
export const PLUS_ONLY_PROVIDERS: CLIProxyProvider[] = ['kiro', 'ghcp'];
|
||||
export const PLUS_ONLY_PROVIDERS: CLIProxyProvider[] = [
|
||||
'kiro',
|
||||
'ghcp',
|
||||
'cursor',
|
||||
'gitlab',
|
||||
'codebuddy',
|
||||
'kilo',
|
||||
];
|
||||
|
||||
/**
|
||||
* CLIProxy config.yaml structure (minimal)
|
||||
|
||||
@@ -183,6 +183,10 @@ export const BUILTIN_PROVIDER_SHORTCUTS: readonly ShortcutEntry[] = CLIPROXY_PRO
|
||||
ghcp: 'GitHub Copilot via CLIProxy OAuth',
|
||||
claude: 'Claude via CLIProxy OAuth',
|
||||
kimi: 'Kimi via CLIProxy OAuth',
|
||||
cursor: 'Cursor via CLIProxy OAuth',
|
||||
gitlab: 'GitLab Duo via CLIProxy OAuth',
|
||||
codebuddy: 'CodeBuddy via CLIProxy OAuth',
|
||||
kilo: 'Kilo AI via CLIProxy OAuth',
|
||||
}[name] || 'CLIProxy OAuth provider',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -9,6 +9,13 @@ export const RESERVED_PROFILE_NAMES = [
|
||||
'agy',
|
||||
'qwen',
|
||||
'iflow',
|
||||
'kiro',
|
||||
'ghcp',
|
||||
'claude',
|
||||
'kimi',
|
||||
'gitlab',
|
||||
'codebuddy',
|
||||
'kilo',
|
||||
// Copilot API (GitHub Copilot proxy)
|
||||
'copilot',
|
||||
// Cursor IDE (Cursor proxy daemon)
|
||||
|
||||
@@ -121,7 +121,7 @@ export function listClaudeExtensionProfiles(): ClaudeExtensionProfileOption[] {
|
||||
'default',
|
||||
...all.accounts,
|
||||
...all.settings,
|
||||
...all.cliproxy,
|
||||
...all.cliproxy.filter((profileName) => profileName !== 'cursor'),
|
||||
...all.cliproxyVariants,
|
||||
];
|
||||
const deduped = [...new Set(orderedNames)];
|
||||
|
||||
@@ -229,6 +229,20 @@ function parseKiroIDCFlow(raw: unknown): { flow: KiroIDCFlow; invalid: boolean }
|
||||
return { flow: normalizeKiroIDCFlow(normalized), invalid: false };
|
||||
}
|
||||
|
||||
function parseGitLabAuthMode(raw: unknown): { mode: 'oauth' | 'pat'; invalid: boolean } {
|
||||
if (raw === undefined || raw === null || raw === '') {
|
||||
return { mode: 'oauth', invalid: false };
|
||||
}
|
||||
if (typeof raw !== 'string') {
|
||||
return { mode: 'oauth', invalid: true };
|
||||
}
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
if (normalized === 'oauth' || normalized === 'pat') {
|
||||
return { mode: normalized, invalid: false };
|
||||
}
|
||||
return { mode: 'oauth', invalid: true };
|
||||
}
|
||||
|
||||
export function getKiroStartIDCValidationError(options: {
|
||||
kiroMethod: KiroAuthMethod;
|
||||
kiroIDCStartUrl?: string;
|
||||
@@ -603,6 +617,13 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
const kiroIDCRegion =
|
||||
typeof requestBody.kiroIDCRegion === 'string' ? requestBody.kiroIDCRegion.trim() : undefined;
|
||||
const kiroIDCFlowRaw = requestBody.kiroIDCFlow;
|
||||
const gitlabAuthModeRaw = requestBody.gitlabAuthMode;
|
||||
const gitlabBaseUrl =
|
||||
typeof requestBody.gitlabBaseUrl === 'string' ? requestBody.gitlabBaseUrl.trim() : undefined;
|
||||
const gitlabPersonalAccessToken =
|
||||
typeof requestBody.gitlabPersonalAccessToken === 'string'
|
||||
? requestBody.gitlabPersonalAccessToken.trim()
|
||||
: undefined;
|
||||
const riskAcknowledgement = requestBody.riskAcknowledgement;
|
||||
const target = getProxyTarget();
|
||||
if (target.isRemote) {
|
||||
@@ -613,6 +634,8 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
const nickname = nicknameRaw?.trim();
|
||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||
const { flow: kiroIDCFlow, invalid: invalidKiroIDCFlow } = parseKiroIDCFlow(kiroIDCFlowRaw);
|
||||
const { mode: gitlabAuthMode, invalid: invalidGitLabAuthMode } =
|
||||
parseGitLabAuthMode(gitlabAuthModeRaw);
|
||||
|
||||
// Validate provider
|
||||
if (!validProviders.includes(provider as CLIProxyProvider)) {
|
||||
@@ -628,6 +651,14 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && invalidGitLabAuthMode) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid gitlabAuthMode. Supported: oauth, pat',
|
||||
code: 'INVALID_GITLAB_AUTH_MODE',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'kiro') {
|
||||
const kiroIDCValidationError = getKiroStartIDCValidationError({
|
||||
kiroMethod,
|
||||
@@ -651,6 +682,89 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && gitlabAuthMode === 'pat') {
|
||||
if (!gitlabPersonalAccessToken) {
|
||||
res.status(400).json({
|
||||
error: 'gitlabPersonalAccessToken is required when gitlabAuthMode=pat',
|
||||
code: 'MISSING_GITLAB_PAT',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const localProvider = provider as CLIProxyProvider;
|
||||
const knownTokenFiles = listProviderTokenSnapshots(localProvider);
|
||||
const response = await fetch(buildProxyUrl(target, '/v0/management/gitlab-auth-url'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...buildManagementHeaders(target),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...(gitlabBaseUrl ? { base_url: gitlabBaseUrl } : {}),
|
||||
personal_access_token: gitlabPersonalAccessToken,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = (await response.json().catch(() => ({}))) as {
|
||||
status?: string;
|
||||
error?: string;
|
||||
};
|
||||
if (!response.ok || data.status !== 'ok') {
|
||||
res.status(response.ok ? 400 : response.status).json({
|
||||
error: data.error || 'GitLab PAT authentication failed',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenSnapshot = findNewTokenSnapshot(
|
||||
listProviderTokenSnapshots(localProvider),
|
||||
knownTokenFiles
|
||||
);
|
||||
if (!tokenSnapshot) {
|
||||
res.status(409).json({
|
||||
error: 'GitLab PAT authentication completed, but CCS could not find the saved token.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const account = registerAccountFromToken(
|
||||
localProvider,
|
||||
getProviderTokenDir(localProvider),
|
||||
nickname,
|
||||
false,
|
||||
tokenSnapshot.file
|
||||
);
|
||||
if (!account) {
|
||||
res.status(409).json({
|
||||
error: 'GitLab PAT authentication succeeded, but account registration failed.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureManagedModelPrefixes([account.provider]);
|
||||
} catch {
|
||||
// Keep auth success path non-fatal when prefix repair cannot run.
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
account: {
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
nickname: account.nickname,
|
||||
provider: account.provider,
|
||||
isDefault: account.isDefault,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
respondInternalError(res, error, 'Failed to start GitLab PAT flow.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const existingAccounts = getProviderAccounts(provider as CLIProxyProvider);
|
||||
const nicknameError = getStartAuthNicknameError(
|
||||
provider as CLIProxyProvider,
|
||||
@@ -681,6 +795,8 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
kiroIDCStartUrl: provider === 'kiro' ? kiroIDCStartUrl : undefined,
|
||||
kiroIDCRegion: provider === 'kiro' ? kiroIDCRegion : undefined,
|
||||
kiroIDCFlow: provider === 'kiro' && kiroMethod === 'idc' ? kiroIDCFlow : undefined,
|
||||
gitlabAuthMode: provider === 'gitlab' ? gitlabAuthMode : undefined,
|
||||
gitlabBaseUrl: provider === 'gitlab' ? gitlabBaseUrl : undefined,
|
||||
fromUI: true, // Enable project selection prompt in UI
|
||||
noIncognito, // Kiro: use normal browser if enabled
|
||||
});
|
||||
@@ -837,9 +953,14 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {};
|
||||
const nicknameRaw = typeof requestBody.nickname === 'string' ? requestBody.nickname : undefined;
|
||||
const kiroMethodRaw = requestBody.kiroMethod;
|
||||
const gitlabAuthModeRaw = requestBody.gitlabAuthMode;
|
||||
const gitlabBaseUrl =
|
||||
typeof requestBody.gitlabBaseUrl === 'string' ? requestBody.gitlabBaseUrl.trim() : undefined;
|
||||
const riskAcknowledgement = requestBody.riskAcknowledgement;
|
||||
const nickname = nicknameRaw?.trim();
|
||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||
const { mode: gitlabAuthMode, invalid: invalidGitLabAuthMode } =
|
||||
parseGitLabAuthMode(gitlabAuthModeRaw);
|
||||
|
||||
// Check remote mode
|
||||
const target = getProxyTarget();
|
||||
@@ -862,6 +983,22 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && invalidGitLabAuthMode) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid gitlabAuthMode. Supported: oauth, pat',
|
||||
code: 'INVALID_GITLAB_AUTH_MODE',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'gitlab' && gitlabAuthMode === 'pat') {
|
||||
res.status(400).json({
|
||||
error: 'GitLab PAT login must use /api/cliproxy/auth/gitlab/start',
|
||||
code: 'GITLAB_PAT_REQUIRES_START',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'agy' && !isAntigravityResponsibilityBypassEnabled()) {
|
||||
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
|
||||
if (!validation.valid) {
|
||||
@@ -900,11 +1037,18 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
provider === 'kiro' && kiroManagementMethod
|
||||
? `&method=${encodeURIComponent(kiroManagementMethod)}`
|
||||
: '';
|
||||
const gitlabQuery =
|
||||
provider === 'gitlab' && gitlabBaseUrl
|
||||
? `&base_url=${encodeURIComponent(gitlabBaseUrl)}`
|
||||
: '';
|
||||
|
||||
// Call CLIProxyAPI to start OAuth and get auth URL
|
||||
// CLIProxyAPI management routes are under /v0/management prefix
|
||||
const response = await fetch(
|
||||
buildProxyUrl(target, `/v0/management/${authUrlProvider}-auth-url?is_webui=true${kiroQuery}`),
|
||||
buildProxyUrl(
|
||||
target,
|
||||
`/v0/management/${authUrlProvider}-auth-url?is_webui=true${kiroQuery}${gitlabQuery}`
|
||||
),
|
||||
{ headers: buildManagementHeaders(target) }
|
||||
);
|
||||
|
||||
|
||||
@@ -93,6 +93,12 @@ describe('ProfileDetector', () => {
|
||||
expect(result.provider).toBe('gemini');
|
||||
});
|
||||
|
||||
it('should detect newly added cliproxy providers', () => {
|
||||
expect(detector.detectProfileType('gitlab').provider).toBe('gitlab');
|
||||
expect(detector.detectProfileType('codebuddy').provider).toBe('codebuddy');
|
||||
expect(detector.detectProfileType('kilo').provider).toBe('kilo');
|
||||
});
|
||||
|
||||
it('should detect settings-based profile from unified config', () => {
|
||||
const settingsPath = path.join(tempDir, 'glm.settings.json');
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({ env: { ANTHROPIC_MODEL: 'glm-4' } }));
|
||||
|
||||
@@ -121,6 +121,13 @@ describe('Backend Selection', () => {
|
||||
assert(types.PLUS_ONLY_PROVIDERS.includes('ghcp'));
|
||||
});
|
||||
|
||||
it('includes the newer CLIProxyAPIPlus providers as plus-only', () => {
|
||||
assert(types.PLUS_ONLY_PROVIDERS.includes('cursor'));
|
||||
assert(types.PLUS_ONLY_PROVIDERS.includes('gitlab'));
|
||||
assert(types.PLUS_ONLY_PROVIDERS.includes('codebuddy'));
|
||||
assert(types.PLUS_ONLY_PROVIDERS.includes('kilo'));
|
||||
});
|
||||
|
||||
it('does not include gemini as plus-only provider', () => {
|
||||
assert(!types.PLUS_ONLY_PROVIDERS.includes('gemini'));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { loadBaseConfig } from '../../../src/cliproxy/base-config-loader';
|
||||
|
||||
describe('base-config-loader new providers', () => {
|
||||
it.each([
|
||||
['cursor', '/api/provider/cursor', 'composer-2'],
|
||||
['gitlab', '/api/provider/gitlab', 'gitlab-duo'],
|
||||
['codebuddy', '/api/provider/codebuddy', 'auto'],
|
||||
['kilo', '/api/provider/kilo', 'kilo/auto'],
|
||||
] as const)('loads base settings for %s', (provider, baseUrlPath, defaultModel) => {
|
||||
const config = loadBaseConfig(provider);
|
||||
|
||||
expect(config.env.ANTHROPIC_BASE_URL).toContain(baseUrlPath);
|
||||
expect(config.env.ANTHROPIC_AUTH_TOKEN).toBe('ccs-internal-managed');
|
||||
expect(config.env.ANTHROPIC_MODEL).toBe(defaultModel);
|
||||
expect(config.env.ANTHROPIC_DEFAULT_OPUS_MODEL.length).toBeGreaterThan(0);
|
||||
expect(config.env.ANTHROPIC_DEFAULT_SONNET_MODEL.length).toBeGreaterThan(0);
|
||||
expect(config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -41,24 +41,38 @@ describe('provider-capabilities', () => {
|
||||
'ghcp',
|
||||
'claude',
|
||||
'kimi',
|
||||
'cursor',
|
||||
'gitlab',
|
||||
'codebuddy',
|
||||
'kilo',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates provider IDs', () => {
|
||||
expect(isCLIProxyProvider('gemini')).toBe(true);
|
||||
expect(isCLIProxyProvider('ghcp')).toBe(true);
|
||||
expect(isCLIProxyProvider('gitlab')).toBe(true);
|
||||
expect(isCLIProxyProvider('not-a-provider')).toBe(false);
|
||||
expect(isCLIProxyProvider('Gemini')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns providers by OAuth flow capability', () => {
|
||||
expect(getProvidersByOAuthFlow('device_code')).toEqual(['qwen', 'kiro', 'ghcp', 'kimi']);
|
||||
expect(getProvidersByOAuthFlow('device_code')).toEqual([
|
||||
'qwen',
|
||||
'kiro',
|
||||
'ghcp',
|
||||
'kimi',
|
||||
'cursor',
|
||||
'codebuddy',
|
||||
'kilo',
|
||||
]);
|
||||
expect(getProvidersByOAuthFlow('authorization_code')).toEqual([
|
||||
'gemini',
|
||||
'codex',
|
||||
'agy',
|
||||
'iflow',
|
||||
'claude',
|
||||
'gitlab',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -69,6 +83,8 @@ describe('provider-capabilities', () => {
|
||||
expect(mapExternalProviderName('github-copilot')).toBe('ghcp');
|
||||
expect(mapExternalProviderName('copilot')).toBe('ghcp');
|
||||
expect(mapExternalProviderName('anthropic')).toBe('claude');
|
||||
expect(mapExternalProviderName('gitlab-duo')).toBe('gitlab');
|
||||
expect(mapExternalProviderName('tencent')).toBe('codebuddy');
|
||||
expect(mapExternalProviderName(' COPILOT ')).toBe('ghcp');
|
||||
expect(mapExternalProviderName('')).toBeNull();
|
||||
expect(mapExternalProviderName('unknown-provider')).toBeNull();
|
||||
@@ -99,8 +115,11 @@ describe('provider-capabilities', () => {
|
||||
it('exposes callback port and display name capabilities', () => {
|
||||
expect(getOAuthCallbackPort('qwen')).toBeNull();
|
||||
expect(getOAuthCallbackPort('kiro')).toBeNull();
|
||||
expect(getOAuthCallbackPort('cursor')).toBeNull();
|
||||
expect(getOAuthCallbackPort('gitlab')).toBe(17171);
|
||||
expect(getOAuthCallbackPort('gemini')).toBe(8085);
|
||||
expect(getProviderDisplayName('agy')).toBe('Antigravity');
|
||||
expect(getProviderDisplayName('kilo')).toBe('Kilo AI');
|
||||
});
|
||||
|
||||
it('throws when provider aliases collide across providers', () => {
|
||||
|
||||
@@ -82,6 +82,9 @@ describe('completion backend', () => {
|
||||
expect(values).toContain('cursor');
|
||||
expect(values).toContain('copilot');
|
||||
expect(values).toContain('gemini');
|
||||
expect(values).toContain('gitlab');
|
||||
expect(values).toContain('codebuddy');
|
||||
expect(values).toContain('kilo');
|
||||
expect(values).toContain('localglm');
|
||||
expect(values).toContain('work');
|
||||
expect(values).toContain('my-codex');
|
||||
|
||||
@@ -50,6 +50,9 @@ describe('help command parity', () => {
|
||||
expect(rendered.includes('gemini')).toBe(true);
|
||||
expect(rendered.includes('codex')).toBe(true);
|
||||
expect(rendered.includes('ghcp')).toBe(true);
|
||||
expect(rendered.includes('gitlab')).toBe(true);
|
||||
expect(rendered.includes('codebuddy')).toBe(true);
|
||||
expect(rendered.includes('kilo')).toBe(true);
|
||||
});
|
||||
|
||||
test('kiro topic documents IDC and callback flags', async () => {
|
||||
|
||||
@@ -13,6 +13,15 @@ describe('cliproxy-auth-routes start-url guard', () => {
|
||||
);
|
||||
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('cursor')).toContain(
|
||||
"Provider 'cursor' uses Device Code flow"
|
||||
);
|
||||
expect(getStartUrlUnsupportedReason('codebuddy')).toContain(
|
||||
"Provider 'codebuddy' uses Device Code flow"
|
||||
);
|
||||
expect(getStartUrlUnsupportedReason('kilo')).toContain(
|
||||
"Provider 'kilo' uses Device Code flow"
|
||||
);
|
||||
});
|
||||
|
||||
it('allows Kiro social methods on start-url', () => {
|
||||
@@ -36,6 +45,7 @@ describe('cliproxy-auth-routes start-url guard', () => {
|
||||
expect(getStartUrlUnsupportedReason('gemini')).toBeNull();
|
||||
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
|
||||
expect(getStartUrlUnsupportedReason('claude')).toBeNull();
|
||||
expect(getStartUrlUnsupportedReason('gitlab')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,6 +97,7 @@ describe('cliproxy-auth-routes start failure messaging', () => {
|
||||
it('keeps generic failure text for other providers', () => {
|
||||
expect(getStartAuthFailureMessage('gemini')).toBe('Authentication failed or was cancelled');
|
||||
expect(getStartAuthFailureMessage('kiro')).toBe('Authentication failed or was cancelled');
|
||||
expect(getStartAuthFailureMessage('gitlab')).toBe('Authentication failed or was cancelled');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect width="64" height="64" rx="16" fill="#EFF6FF"/>
|
||||
<rect x="14" y="16" width="36" height="32" rx="10" fill="#2563EB"/>
|
||||
<path d="M24 28h16M24 36h10" stroke="#DBEAFE" stroke-width="4" stroke-linecap="round"/>
|
||||
<circle cx="46" cy="42" r="8" fill="#0F172A"/>
|
||||
<path d="M43 42h6M46 39v6" stroke="#F8FAFC" stroke-width="3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect width="64" height="64" rx="16" fill="#111827"/>
|
||||
<path d="M44 21.5a17.4 17.4 0 0 0-11.7-4.5c-9.6 0-17.3 7.6-17.3 17s7.7 17 17.3 17c4.6 0 8.9-1.7 12.1-4.9" stroke="#F9FAFB" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M37.5 25.5 46 32l-8.5 6.5" stroke="#22C55E" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect width="64" height="64" rx="16" fill="#FFF7ED"/>
|
||||
<path d="M18 44 32 17l14 27-14 10-14-10Z" fill="#FC6D26"/>
|
||||
<path d="m18 44 5.4-16.2 8.6 26.2L18 44Zm28 0-5.4-16.2L32 54l14-10Z" fill="#E24329"/>
|
||||
<path d="M23.4 27.8 32 17l8.6 10.8H23.4Z" fill="#FCA326"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect width="64" height="64" rx="16" fill="#FFF1F2"/>
|
||||
<path d="M20 16h8v14l12-14h10L36.5 31 51 48H40.3L28 33.6V48h-8V16Z" fill="#E11D48"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 223 B |
@@ -91,6 +91,9 @@ export function AddAccountDialog({
|
||||
const [kiroIDCStartUrl, setKiroIDCStartUrl] = useState('');
|
||||
const [kiroIDCRegion, setKiroIDCRegion] = useState('');
|
||||
const [kiroIDCFlow, setKiroIDCFlow] = useState<KiroIDCFlow>(DEFAULT_KIRO_IDC_FLOW);
|
||||
const [gitlabAuthMode, setGitlabAuthMode] = useState<'oauth' | 'pat'>('oauth');
|
||||
const [gitlabBaseUrl, setGitlabBaseUrl] = useState('');
|
||||
const [gitlabPersonalAccessToken, setGitlabPersonalAccessToken] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const wasAuthenticatingRef = useRef(false);
|
||||
const powerUserModeRequestIdRef = useRef(0);
|
||||
@@ -99,6 +102,7 @@ export function AddAccountDialog({
|
||||
const kiroImportMutation = useKiroImport();
|
||||
|
||||
const isKiro = provider === 'kiro';
|
||||
const isGitLab = provider === 'gitlab';
|
||||
const supportsPowerUserMode = provider === 'agy' || provider === 'gemini';
|
||||
const requiresGeminiSafetyAcknowledgement = provider === 'gemini' && !powerUserModeEnabled;
|
||||
const requiresAgyResponsibilityFlow = provider === 'agy' && !powerUserModeEnabled;
|
||||
@@ -120,6 +124,8 @@ export function AddAccountDialog({
|
||||
const nicknameTrimmed = nickname.trim();
|
||||
const kiroIDCStartUrlTrimmed = kiroIDCStartUrl.trim();
|
||||
const kiroIDCRegionTrimmed = kiroIDCRegion.trim();
|
||||
const gitlabBaseUrlTrimmed = gitlabBaseUrl.trim();
|
||||
const gitlabPersonalAccessTokenTrimmed = gitlabPersonalAccessToken.trim();
|
||||
const errorMessage = localError || authFlow.error;
|
||||
|
||||
const fetchPowerUserModeState = useCallback(async (): Promise<boolean> => {
|
||||
@@ -191,6 +197,9 @@ export function AddAccountDialog({
|
||||
setKiroIDCStartUrl('');
|
||||
setKiroIDCRegion('');
|
||||
setKiroIDCFlow(DEFAULT_KIRO_IDC_FLOW);
|
||||
setGitlabAuthMode('oauth');
|
||||
setGitlabBaseUrl('');
|
||||
setGitlabPersonalAccessToken('');
|
||||
powerUserModeRequestIdRef.current += 1;
|
||||
powerUserModeLoadErrorShownRef.current = false;
|
||||
wasAuthenticatingRef.current = false;
|
||||
@@ -310,6 +319,10 @@ export function AddAccountDialog({
|
||||
setLocalError('IDC Start URL is required for Kiro IAM Identity Center login.');
|
||||
return;
|
||||
}
|
||||
if (isGitLab && gitlabAuthMode === 'pat' && !gitlabPersonalAccessTokenTrimmed) {
|
||||
setLocalError('GitLab Personal Access Token is required for PAT login.');
|
||||
return;
|
||||
}
|
||||
wasAuthenticatingRef.current = true;
|
||||
authFlow.startAuth(provider, {
|
||||
nickname: nicknameTrimmed || undefined,
|
||||
@@ -317,8 +330,15 @@ export function AddAccountDialog({
|
||||
kiroIDCStartUrl: isKiroIdc ? kiroIDCStartUrlTrimmed : undefined,
|
||||
kiroIDCRegion: isKiroIdc && kiroIDCRegionTrimmed ? kiroIDCRegionTrimmed : undefined,
|
||||
kiroIDCFlow: isKiroIdc ? kiroIDCFlow : undefined,
|
||||
gitlabAuthMode: isGitLab ? gitlabAuthMode : undefined,
|
||||
gitlabBaseUrl: isGitLab && gitlabBaseUrlTrimmed ? gitlabBaseUrlTrimmed : undefined,
|
||||
gitlabPersonalAccessToken:
|
||||
isGitLab && gitlabAuthMode === 'pat' && gitlabPersonalAccessTokenTrimmed
|
||||
? gitlabPersonalAccessTokenTrimmed
|
||||
: undefined,
|
||||
flowType: isKiro ? selectedKiroFlowType : undefined,
|
||||
startEndpoint: isKiro ? selectedKiroStartEndpoint : undefined,
|
||||
startEndpoint:
|
||||
isKiro ? selectedKiroStartEndpoint : isGitLab && gitlabAuthMode === 'pat' ? 'start' : undefined,
|
||||
riskAcknowledgement: requiresAgyResponsibilityFlow
|
||||
? {
|
||||
version: ANTIGRAVITY_ACK_VERSION,
|
||||
@@ -512,6 +532,70 @@ export function AddAccountDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGitLab && !showAuthUI && (
|
||||
<div className="space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gitlab-auth-mode">GitLab auth method</Label>
|
||||
<Select
|
||||
value={gitlabAuthMode}
|
||||
onValueChange={(value) => {
|
||||
setGitlabAuthMode(value as 'oauth' | 'pat');
|
||||
setLocalError(null);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="gitlab-auth-mode">
|
||||
<SelectValue placeholder="Select GitLab auth method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="oauth">Browser OAuth</SelectItem>
|
||||
<SelectItem value="pat">Personal Access Token</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Use Browser OAuth for gitlab.com, or PAT for self-hosted and admin-managed setups.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gitlab-base-url">GitLab URL</Label>
|
||||
<Input
|
||||
id="gitlab-base-url"
|
||||
value={gitlabBaseUrl}
|
||||
onChange={(e) => {
|
||||
setGitlabBaseUrl(e.target.value);
|
||||
setLocalError(null);
|
||||
}}
|
||||
placeholder="https://gitlab.com"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Optional. Leave blank for gitlab.com, or set your self-hosted GitLab base URL.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{gitlabAuthMode === 'pat' && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gitlab-pat">Personal Access Token</Label>
|
||||
<Input
|
||||
id="gitlab-pat"
|
||||
type="password"
|
||||
value={gitlabPersonalAccessToken}
|
||||
onChange={(e) => {
|
||||
setGitlabPersonalAccessToken(e.target.value);
|
||||
setLocalError(null);
|
||||
}}
|
||||
placeholder="glpat-..."
|
||||
disabled={isPending}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Token must include at least <span className="font-mono">api</span> and{' '}
|
||||
<span className="font-mono">read_user</span> scopes.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nickname input - only show before auth starts */}
|
||||
{!showAuthUI && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -86,8 +86,12 @@ export function ProviderEditor({
|
||||
gemini: ['google'],
|
||||
agy: ['antigravity'],
|
||||
codex: ['openai'],
|
||||
cursor: ['cursor'],
|
||||
gitlab: ['gitlab', 'duo'],
|
||||
codebuddy: ['codebuddy'],
|
||||
qwen: ['alibaba', 'qwen'],
|
||||
iflow: ['iflow'],
|
||||
kilo: ['kilo'],
|
||||
kiro: ['kiro', 'aws'],
|
||||
ghcp: ['github', 'copilot'],
|
||||
kimi: ['kimi', 'moonshot'],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Constants for Quick Setup Wizard
|
||||
* Provider display info with custom ordering for wizard UI.
|
||||
* Provider IDs must match CLIPROXY_PROVIDERS from provider-config.ts
|
||||
* IDs come from the provider-config presentation layer.
|
||||
*/
|
||||
|
||||
import type { ProviderOption } from './types';
|
||||
@@ -17,9 +17,13 @@ const WIZARD_PROVIDER_ORDER: CLIProxyProvider[] = [
|
||||
'claude',
|
||||
'gemini',
|
||||
'codex',
|
||||
'cursor',
|
||||
'gitlab',
|
||||
'codebuddy',
|
||||
'qwen',
|
||||
'kimi',
|
||||
'iflow',
|
||||
'kilo',
|
||||
'kiro',
|
||||
'ghcp',
|
||||
];
|
||||
|
||||
@@ -30,6 +30,9 @@ interface StartAuthOptions {
|
||||
kiroIDCStartUrl?: string;
|
||||
kiroIDCRegion?: string;
|
||||
kiroIDCFlow?: 'authcode' | 'device';
|
||||
gitlabAuthMode?: 'oauth' | 'pat';
|
||||
gitlabBaseUrl?: string;
|
||||
gitlabPersonalAccessToken?: string;
|
||||
flowType?: 'authorization_code' | 'device_code';
|
||||
startEndpoint?: 'start' | 'start-url';
|
||||
riskAcknowledgement?: {
|
||||
@@ -272,6 +275,9 @@ export function useCliproxyAuthFlow() {
|
||||
kiroIDCStartUrl: options?.kiroIDCStartUrl,
|
||||
kiroIDCRegion: options?.kiroIDCRegion,
|
||||
kiroIDCFlow: options?.kiroIDCFlow,
|
||||
gitlabAuthMode: options?.gitlabAuthMode,
|
||||
gitlabBaseUrl: options?.gitlabBaseUrl,
|
||||
gitlabPersonalAccessToken: options?.gitlabPersonalAccessToken,
|
||||
riskAcknowledgement: options?.riskAcknowledgement,
|
||||
};
|
||||
|
||||
|
||||
@@ -16,12 +16,10 @@ import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/clip
|
||||
|
||||
/** Canonical list of CLIProxy provider IDs (shared with backend). */
|
||||
export const CLIPROXY_PROVIDERS = CLIPROXY_PROVIDER_IDS;
|
||||
|
||||
/** Union type for CLIProxy provider IDs */
|
||||
export type CLIProxyProvider = (typeof CLIPROXY_PROVIDERS)[number];
|
||||
export type ProviderVisualId = CLIProxyProvider | 'openai' | 'vertex';
|
||||
|
||||
/** Check if a string is a valid CLIProxy provider */
|
||||
/** Check if a string is a backend-supported CLIProxy provider. */
|
||||
export function isValidProvider(provider: string): provider is CLIProxyProvider {
|
||||
return CLIPROXY_PROVIDERS.includes(provider as CLIProxyProvider);
|
||||
}
|
||||
@@ -37,9 +35,13 @@ interface ProviderMetadata {
|
||||
|
||||
const SPECIAL_PROVIDER_VISUAL_IDS = ['openai', 'vertex'] as const;
|
||||
|
||||
function isPresentationProvider(provider: string): provider is CLIProxyProvider {
|
||||
return isValidProvider(provider);
|
||||
}
|
||||
|
||||
function isProviderVisualId(provider: string): provider is ProviderVisualId {
|
||||
return (
|
||||
isValidProvider(provider) ||
|
||||
isPresentationProvider(provider) ||
|
||||
SPECIAL_PROVIDER_VISUAL_IDS.includes(provider as (typeof SPECIAL_PROVIDER_VISUAL_IDS)[number])
|
||||
);
|
||||
}
|
||||
@@ -64,6 +66,10 @@ export const PROVIDER_ASSETS: Partial<Record<ProviderVisualId, string>> = {
|
||||
qwen: '/assets/providers/qwen-color.svg',
|
||||
iflow: '/assets/providers/iflow.png',
|
||||
kiro: '/assets/providers/kiro.png',
|
||||
cursor: '/assets/providers/cursor.svg',
|
||||
gitlab: '/assets/providers/gitlab.svg',
|
||||
codebuddy: '/assets/providers/codebuddy.svg',
|
||||
kilo: '/assets/providers/kilo.svg',
|
||||
ghcp: '/assets/providers/copilot.svg',
|
||||
claude: '/assets/providers/claude.svg',
|
||||
kimi: '/assets/providers/kimi.svg',
|
||||
@@ -90,6 +96,10 @@ export const PROVIDER_FALLBACK_VISUALS: Record<ProviderVisualId, ProviderFallbac
|
||||
qwen: { textClass: 'text-cyan-600', letter: 'Q' },
|
||||
iflow: { textClass: 'text-indigo-600', letter: 'i' },
|
||||
kiro: { textClass: 'text-teal-600', letter: 'K' },
|
||||
cursor: { textClass: 'text-slate-900', letter: 'C' },
|
||||
gitlab: { textClass: 'text-orange-600', letter: 'G' },
|
||||
codebuddy: { textClass: 'text-blue-600', letter: 'B' },
|
||||
kilo: { textClass: 'text-rose-600', letter: 'K' },
|
||||
ghcp: { textClass: 'text-green-600', letter: 'C' },
|
||||
kimi: { textClass: 'text-orange-500', letter: 'K' },
|
||||
openai: { textClass: 'text-slate-900', letter: 'O' },
|
||||
@@ -223,6 +233,10 @@ export const PROVIDER_COLORS: Record<string, string> = {
|
||||
iflow: '#f94144',
|
||||
qwen: '#6236FF',
|
||||
kiro: '#4d908e',
|
||||
cursor: '#111827',
|
||||
gitlab: '#FC6D26',
|
||||
codebuddy: '#2563EB',
|
||||
kilo: '#E11D48',
|
||||
ghcp: '#43aa8b',
|
||||
claude: '#D97757',
|
||||
kimi: '#FF6B35',
|
||||
@@ -247,7 +261,7 @@ export function getProviderDisplayName(provider: unknown): string {
|
||||
/** Map provider to user-facing short description */
|
||||
export function getProviderDescription(provider: unknown): string {
|
||||
const normalized = normalizeProviderInput(provider);
|
||||
if (!isValidProvider(normalized)) return '';
|
||||
if (!isPresentationProvider(normalized)) return '';
|
||||
return PROVIDER_METADATA[normalized].description;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,22 @@ describe('AddAccountDialog power user mode', () => {
|
||||
vi.clearAllMocks();
|
||||
fetchMock.mockReset();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
Object.defineProperty(HTMLElement.prototype, 'hasPointerCapture', {
|
||||
configurable: true,
|
||||
value: () => false,
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, 'setPointerCapture', {
|
||||
configurable: true,
|
||||
value: () => {},
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, 'releasePointerCapture', {
|
||||
configurable: true,
|
||||
value: () => {},
|
||||
});
|
||||
Object.defineProperty(Element.prototype, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value: () => {},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -168,4 +184,26 @@ describe('AddAccountDialog power user mode', () => {
|
||||
expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument();
|
||||
expect(authMocks.startAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits GitLab PAT mode with base URL and token through the shared auth hook', async () => {
|
||||
render(<AddAccountDialog open onClose={vi.fn()} provider="gitlab" displayName="GitLab Duo" />);
|
||||
|
||||
await userEvent.click(screen.getByRole('combobox', { name: 'GitLab auth method' }));
|
||||
await userEvent.click(screen.getByRole('option', { name: 'Personal Access Token' }));
|
||||
|
||||
await userEvent.type(screen.getByLabelText('GitLab URL'), 'https://gitlab.example.com');
|
||||
await userEvent.type(screen.getByLabelText('Personal Access Token'), 'glpat-test-token');
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Authenticate' }));
|
||||
|
||||
expect(authMocks.startAuth).toHaveBeenCalledWith(
|
||||
'gitlab',
|
||||
expect.objectContaining({
|
||||
gitlabAuthMode: 'pat',
|
||||
gitlabBaseUrl: 'https://gitlab.example.com',
|
||||
gitlabPersonalAccessToken: 'glpat-test-token',
|
||||
startEndpoint: 'start',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -307,6 +307,56 @@ describe('useCliproxyAuthFlow', () => {
|
||||
kiroIDCStartUrl: 'https://d-123.awsapps.com/start',
|
||||
kiroIDCRegion: 'ca-central-1',
|
||||
kiroIDCFlow: 'authcode',
|
||||
gitlabAuthMode: undefined,
|
||||
gitlabBaseUrl: undefined,
|
||||
gitlabPersonalAccessToken: undefined,
|
||||
riskAcknowledgement: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards GitLab PAT options to the backend start endpoint payload', async () => {
|
||||
let requestBody: Record<string, unknown> | null = null;
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
|
||||
if (url.includes('/start')) {
|
||||
requestBody = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
||||
return createJsonResponse({
|
||||
success: true,
|
||||
account: {
|
||||
id: 'gitlab-account',
|
||||
provider: 'gitlab',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useCliproxyAuthFlow(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.startAuth('gitlab', {
|
||||
startEndpoint: 'start',
|
||||
gitlabAuthMode: 'pat',
|
||||
gitlabBaseUrl: 'https://gitlab.example.com',
|
||||
gitlabPersonalAccessToken: 'glpat-test-token',
|
||||
});
|
||||
});
|
||||
|
||||
expect(requestBody).toEqual({
|
||||
nickname: undefined,
|
||||
kiroMethod: undefined,
|
||||
kiroIDCStartUrl: undefined,
|
||||
kiroIDCRegion: undefined,
|
||||
kiroIDCFlow: undefined,
|
||||
gitlabAuthMode: 'pat',
|
||||
gitlabBaseUrl: 'https://gitlab.example.com',
|
||||
gitlabPersonalAccessToken: 'glpat-test-token',
|
||||
riskAcknowledgement: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,14 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
formatRequestedUpstreamModelRules,
|
||||
getProviderDescription,
|
||||
getProviderDisplayName,
|
||||
getProviderFallbackVisual,
|
||||
getProviderLogoAsset,
|
||||
getRequestedUpstreamModelRuleErrors,
|
||||
getRequestedModelId,
|
||||
parseRequestedUpstreamModelRules,
|
||||
PROVIDER_COLORS,
|
||||
} from '@/lib/provider-config';
|
||||
|
||||
describe('provider model mapping helpers', () => {
|
||||
@@ -42,3 +47,45 @@ describe('provider model mapping helpers', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider presentation metadata', () => {
|
||||
it.each([
|
||||
['cursor', 'Cursor', 'Cursor browser-authenticated provider', '/assets/providers/cursor.svg'],
|
||||
['gitlab', 'GitLab Duo', 'GitLab Duo with OAuth or PAT auth', '/assets/providers/gitlab.svg'],
|
||||
[
|
||||
'codebuddy',
|
||||
'CodeBuddy (Tencent)',
|
||||
'Tencent CodeBuddy AI assistant',
|
||||
'/assets/providers/codebuddy.svg',
|
||||
],
|
||||
['kilo', 'Kilo AI', 'Kilo AI coding assistant', '/assets/providers/kilo.svg'],
|
||||
])('recognizes %s across dashboard display helpers', (provider, name, description, asset) => {
|
||||
expect(getProviderDisplayName(provider)).toBe(name);
|
||||
expect(getProviderDescription(provider)).toBe(description);
|
||||
expect(getProviderLogoAsset(provider)).toBe(asset);
|
||||
});
|
||||
|
||||
it('provides fallback visuals and brand colors for new providers', () => {
|
||||
expect(getProviderFallbackVisual('cursor')).toEqual({
|
||||
textClass: 'text-slate-900',
|
||||
letter: 'C',
|
||||
});
|
||||
expect(getProviderFallbackVisual('gitlab')).toEqual({
|
||||
textClass: 'text-orange-600',
|
||||
letter: 'G',
|
||||
});
|
||||
expect(getProviderFallbackVisual('codebuddy')).toEqual({
|
||||
textClass: 'text-blue-600',
|
||||
letter: 'B',
|
||||
});
|
||||
expect(getProviderFallbackVisual('kilo')).toEqual({
|
||||
textClass: 'text-rose-600',
|
||||
letter: 'K',
|
||||
});
|
||||
|
||||
expect(PROVIDER_COLORS.cursor).toBe('#111827');
|
||||
expect(PROVIDER_COLORS.gitlab).toBe('#FC6D26');
|
||||
expect(PROVIDER_COLORS.codebuddy).toBe('#2563EB');
|
||||
expect(PROVIDER_COLORS.kilo).toBe('#E11D48');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user