mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
feat(cliproxy): integrate missing provider support
This commit is contained in:
1 parent
2589c02937
commit
2d9f8c9695
34 files changed
+863
-34
No files matched your search
@@ -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
|
* Detect profile type and return routing information
|
||||||
*
|
*
|
||||||
* Priority order:
|
* Priority order:
|
||||||
* 0. Hardcoded CLIProxy profiles (gemini, codex, agy, qwen)
|
* 0. Hardcoded special runtime profiles (copilot, cursor)
|
||||||
* 0.5. Copilot profile (if enabled in config)
|
* 0.5. Hardcoded CLIProxy profiles (gemini, codex, agy, qwen, ...)
|
||||||
* 0.75. Cursor profile (if enabled in config)
|
|
||||||
* 1. Unified config profiles (if config.yaml exists or CCS_UNIFIED_CONFIG=1)
|
* 1. Unified config profiles (if config.yaml exists or CCS_UNIFIED_CONFIG=1)
|
||||||
* 2. User-defined CLIProxy variants (config.cliproxy section) [legacy]
|
* 2. User-defined CLIProxy variants (config.cliproxy section) [legacy]
|
||||||
* 3. Settings-based profiles (config.profiles section) [legacy]
|
* 3. Settings-based profiles (config.profiles section) [legacy]
|
||||||
@@ -267,16 +266,7 @@ class ProfileDetector {
|
|||||||
return this.resolveDefaultProfile();
|
return this.resolveDefaultProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 0: Check CLIProxy profiles (gemini, codex, agy, qwen) - OAuth-based, zero config
|
// Priority 0: Check Copilot profile - GitHub Copilot subscription via copilot-api
|
||||||
if (isCLIProxyProvider(profileName)) {
|
|
||||||
return {
|
|
||||||
type: 'cliproxy',
|
|
||||||
name: profileName,
|
|
||||||
provider: profileName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 0.5: Check Copilot profile - GitHub Copilot subscription via copilot-api
|
|
||||||
if (profileName === 'copilot') {
|
if (profileName === 'copilot') {
|
||||||
const unifiedConfig = this.readUnifiedConfig();
|
const unifiedConfig = this.readUnifiedConfig();
|
||||||
const copilotConfig = unifiedConfig?.copilot;
|
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') {
|
if (profileName === 'cursor') {
|
||||||
const cursorConfig = getCursorConfig();
|
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
|
// Priority 1: Try unified config if available
|
||||||
const unifiedConfig = this.readUnifiedConfig();
|
const unifiedConfig = this.readUnifiedConfig();
|
||||||
if (unifiedConfig) {
|
if (unifiedConfig) {
|
||||||
|
|||||||
+20
-2
@@ -77,6 +77,7 @@ import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils
|
|||||||
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
|
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
|
||||||
import { createLogger } from './services/logging';
|
import { createLogger } from './services/logging';
|
||||||
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
|
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
|
||||||
|
import type { ProfileDetectionResult } from './auth/profile-detector';
|
||||||
|
|
||||||
// Import target adapter system
|
// Import target adapter system
|
||||||
import {
|
import {
|
||||||
@@ -122,6 +123,7 @@ interface RuntimeReasoningResolution {
|
|||||||
sourceDisplay: string | undefined;
|
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_RUNTIME_REASONING_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
|
||||||
const CODEX_NATIVE_PASSTHROUGH_FLAGS = new Set(['--help', '-h', '--version', '-v']);
|
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(
|
function resolveRuntimeReasoningFlags(
|
||||||
args: string[],
|
args: string[],
|
||||||
envThinkingValue: string | undefined
|
envThinkingValue: string | undefined
|
||||||
@@ -537,8 +546,17 @@ async function main(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
// Detect profile (strip --target flags before profile detection)
|
// Detect profile (strip --target flags before profile detection)
|
||||||
const cleanArgs = stripTargetFlag(args);
|
const cleanArgs = stripTargetFlag(args);
|
||||||
const { profile, remainingArgs } = detectProfile(cleanArgs);
|
const useCursorCliproxyShortcut = shouldUseCursorCliproxyShortcut(cleanArgs);
|
||||||
const profileInfo = detector.detectProfileType(profile);
|
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>;
|
let resolvedTarget: ReturnType<typeof resolveTargetType>;
|
||||||
try {
|
try {
|
||||||
resolvedTarget = resolveTargetType(
|
resolvedTarget = resolveTargetType(
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google'
|
|||||||
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
|
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
|
||||||
* - GHCP: 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)
|
* - 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>> =
|
export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> =
|
||||||
CLIPROXY_PROVIDER_IDS.reduce(
|
CLIPROXY_PROVIDER_IDS.reduce(
|
||||||
@@ -274,6 +278,34 @@ export const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
|
|||||||
scopes: ['api'],
|
scopes: ['api'],
|
||||||
authFlag: '--kimi-login',
|
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;
|
noIncognito?: boolean;
|
||||||
/** If true, skip OAuth and import token from Kiro IDE directly (Kiro only) */
|
/** If true, skip OAuth and import token from Kiro IDE directly (Kiro only) */
|
||||||
import?: boolean;
|
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 */
|
/** Enable paste-callback mode: show auth URL and prompt for callback paste */
|
||||||
pasteCallback?: boolean;
|
pasteCallback?: boolean;
|
||||||
/** If true, use port-forwarding mode (skip interactive prompt in headless) */
|
/** 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(
|
export async function requestPasteCallbackStart(
|
||||||
provider: CLIProxyProvider,
|
provider: CLIProxyProvider,
|
||||||
target: ProxyTarget,
|
target: ProxyTarget,
|
||||||
options?: { kiroMethod?: OAuthOptions['kiroMethod'] }
|
options?: {
|
||||||
|
kiroMethod?: OAuthOptions['kiroMethod'];
|
||||||
|
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||||
|
}
|
||||||
): Promise<PasteCallbackStartData> {
|
): Promise<PasteCallbackStartData> {
|
||||||
const startPath = getPasteCallbackStartPath(provider, {
|
let startPath = getPasteCallbackStartPath(provider, {
|
||||||
kiroMethod: options?.kiroMethod,
|
kiroMethod: options?.kiroMethod,
|
||||||
});
|
});
|
||||||
if (!startPath) {
|
if (!startPath) {
|
||||||
@@ -93,6 +96,9 @@ export async function requestPasteCallbackStart(
|
|||||||
`Paste-callback start is not available for ${provider} with the selected method`
|
`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), {
|
const response = await fetch(buildProxyUrl(target, startPath), {
|
||||||
headers: buildManagementHeaders(target),
|
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(
|
export function findNewTokenSnapshotForManualAuth(
|
||||||
provider: CLIProxyProvider,
|
provider: CLIProxyProvider,
|
||||||
tokenDir: string,
|
tokenDir: string,
|
||||||
@@ -458,7 +485,10 @@ async function handlePasteCallbackMode(
|
|||||||
tokenDir: string,
|
tokenDir: string,
|
||||||
nickname?: string,
|
nickname?: string,
|
||||||
expectedAccountId?: string,
|
expectedAccountId?: string,
|
||||||
options?: { kiroMethod?: OAuthOptions['kiroMethod'] }
|
options?: {
|
||||||
|
kiroMethod?: OAuthOptions['kiroMethod'];
|
||||||
|
gitlabBaseUrl?: OAuthOptions['gitlabBaseUrl'];
|
||||||
|
}
|
||||||
): Promise<AccountInfo | null> {
|
): Promise<AccountInfo | null> {
|
||||||
// Resolve CLIProxyAPI target (local or remote based on config)
|
// Resolve CLIProxyAPI target (local or remote based on config)
|
||||||
const target = getProxyTarget();
|
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
|
* Trigger OAuth flow for provider
|
||||||
* Auto-detects headless environment and uses --no-browser flag accordingly
|
* 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;
|
provider === 'kiro' ? normalizeKiroAuthMethod(options.kiroMethod) : DEFAULT_KIRO_AUTH_METHOD;
|
||||||
const resolvedKiroIDCFlow =
|
const resolvedKiroIDCFlow =
|
||||||
provider === 'kiro' ? normalizeKiroIDCFlow(options.kiroIDCFlow) : DEFAULT_KIRO_IDC_FLOW;
|
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 (provider === 'agy') {
|
||||||
if (fromUI && !acceptAgyRisk) {
|
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 =
|
const useSelectedKiroLocalPasteCallback =
|
||||||
selectedPasteCallback &&
|
selectedPasteCallback &&
|
||||||
provider === 'kiro' &&
|
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) {
|
if (selectedPasteCallback && !useSelectedKiroDirectCliFlow) {
|
||||||
const tokenDir = getProviderTokenDir(provider);
|
const tokenDir = getProviderTokenDir(provider);
|
||||||
return handlePasteCallbackMode(
|
return handlePasteCallbackMode(
|
||||||
@@ -774,7 +923,10 @@ export async function triggerOAuth(
|
|||||||
tokenDir,
|
tokenDir,
|
||||||
nickname,
|
nickname,
|
||||||
existingNameMatch?.id,
|
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 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 acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
|
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
|
||||||
|
|
||||||
const incognitoFlag = argsWithoutProxy.includes('--incognito');
|
const incognitoFlag = argsWithoutProxy.includes('--incognito');
|
||||||
@@ -444,6 +445,16 @@ export async function execClaudeWithCLIProxy(
|
|||||||
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
|
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')) {
|
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
|
||||||
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
@@ -491,6 +502,13 @@ export async function execClaudeWithCLIProxy(
|
|||||||
return;
|
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)
|
// Parse --thinking / --effort flags (aliases; first occurrence wins)
|
||||||
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
|
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
|
||||||
if (thinkingParse.error) {
|
if (thinkingParse.error) {
|
||||||
@@ -730,6 +748,8 @@ export async function execClaudeWithCLIProxy(
|
|||||||
...(kiroIDCStartUrl && p === 'kiro' ? { kiroIDCStartUrl } : {}),
|
...(kiroIDCStartUrl && p === 'kiro' ? { kiroIDCStartUrl } : {}),
|
||||||
...(kiroIDCRegion && p === 'kiro' ? { kiroIDCRegion } : {}),
|
...(kiroIDCRegion && p === 'kiro' ? { kiroIDCRegion } : {}),
|
||||||
...(kiroIDCFlow && p === 'kiro' ? { kiroIDCFlow } : {}),
|
...(kiroIDCFlow && p === 'kiro' ? { kiroIDCFlow } : {}),
|
||||||
|
...(gitlabTokenLogin && p === 'gitlab' ? { gitlabAuthMode: 'pat' as const } : {}),
|
||||||
|
...(gitlabBaseUrl && p === 'gitlab' ? { gitlabBaseUrl } : {}),
|
||||||
...(forceHeadless ? { headless: true } : {}),
|
...(forceHeadless ? { headless: true } : {}),
|
||||||
...(setNickname ? { nickname: setNickname } : {}),
|
...(setNickname ? { nickname: setNickname } : {}),
|
||||||
...(noIncognito ? { noIncognito: true } : {}),
|
...(noIncognito ? { noIncognito: true } : {}),
|
||||||
@@ -775,6 +795,8 @@ export async function execClaudeWithCLIProxy(
|
|||||||
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
|
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
|
||||||
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
|
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
|
||||||
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
|
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
|
||||||
|
...(gitlabTokenLogin ? { gitlabAuthMode: 'pat' as const } : {}),
|
||||||
|
...(gitlabBaseUrl ? { gitlabBaseUrl } : {}),
|
||||||
...(forceHeadless ? { headless: true } : {}),
|
...(forceHeadless ? { headless: true } : {}),
|
||||||
...(setNickname ? { nickname: setNickname } : {}),
|
...(setNickname ? { nickname: setNickname } : {}),
|
||||||
...(noIncognito ? { noIncognito: true } : {}),
|
...(noIncognito ? { noIncognito: true } : {}),
|
||||||
|
|||||||
@@ -134,6 +134,54 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
|||||||
tokenTypeValues: ['kimi'],
|
tokenTypeValues: ['kimi'],
|
||||||
aliases: ['moonshot'],
|
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(
|
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)
|
* - ghcp: GitHub Copilot via Device Code (OAuth through CLIProxyAPIPlus)
|
||||||
* - claude: Claude (Anthropic) via OAuth
|
* - claude: Claude (Anthropic) via OAuth
|
||||||
* - kimi: Kimi (Moonshot AI) via Device Code 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 =
|
export type CLIProxyProvider =
|
||||||
| 'gemini'
|
| 'gemini'
|
||||||
@@ -132,12 +136,16 @@ export type CLIProxyProvider =
|
|||||||
| 'kiro'
|
| 'kiro'
|
||||||
| 'ghcp'
|
| 'ghcp'
|
||||||
| 'claude'
|
| 'claude'
|
||||||
| 'kimi';
|
| 'kimi'
|
||||||
|
| 'cursor'
|
||||||
|
| 'gitlab'
|
||||||
|
| 'codebuddy'
|
||||||
|
| 'kilo';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLIProxy backend selection
|
* CLIProxy backend selection
|
||||||
* - original: CLIProxyAPI (no Kiro/ghcp support)
|
* - original: CLIProxyAPI (legacy provider subset)
|
||||||
* - plus: CLIProxyAPIPlus (Kiro/ghcp support, default)
|
* - plus: CLIProxyAPIPlus (expanded provider support, default)
|
||||||
*/
|
*/
|
||||||
export type CLIProxyBackend = 'original' | 'plus';
|
export type CLIProxyBackend = 'original' | 'plus';
|
||||||
|
|
||||||
@@ -149,7 +157,14 @@ export type CliproxyRoutingStrategy = 'round-robin' | 'fill-first';
|
|||||||
/**
|
/**
|
||||||
* Providers that require CLIProxyAPIPlus backend
|
* 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)
|
* CLIProxy config.yaml structure (minimal)
|
||||||
|
|||||||
@@ -183,6 +183,10 @@ export const BUILTIN_PROVIDER_SHORTCUTS: readonly ShortcutEntry[] = CLIPROXY_PRO
|
|||||||
ghcp: 'GitHub Copilot via CLIProxy OAuth',
|
ghcp: 'GitHub Copilot via CLIProxy OAuth',
|
||||||
claude: 'Claude via CLIProxy OAuth',
|
claude: 'Claude via CLIProxy OAuth',
|
||||||
kimi: 'Kimi 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',
|
}[name] || 'CLIProxy OAuth provider',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ export const RESERVED_PROFILE_NAMES = [
|
|||||||
'agy',
|
'agy',
|
||||||
'qwen',
|
'qwen',
|
||||||
'iflow',
|
'iflow',
|
||||||
|
'kiro',
|
||||||
|
'ghcp',
|
||||||
|
'claude',
|
||||||
|
'kimi',
|
||||||
|
'gitlab',
|
||||||
|
'codebuddy',
|
||||||
|
'kilo',
|
||||||
// Copilot API (GitHub Copilot proxy)
|
// Copilot API (GitHub Copilot proxy)
|
||||||
'copilot',
|
'copilot',
|
||||||
// Cursor IDE (Cursor proxy daemon)
|
// Cursor IDE (Cursor proxy daemon)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function listClaudeExtensionProfiles(): ClaudeExtensionProfileOption[] {
|
|||||||
'default',
|
'default',
|
||||||
...all.accounts,
|
...all.accounts,
|
||||||
...all.settings,
|
...all.settings,
|
||||||
...all.cliproxy,
|
...all.cliproxy.filter((profileName) => profileName !== 'cursor'),
|
||||||
...all.cliproxyVariants,
|
...all.cliproxyVariants,
|
||||||
];
|
];
|
||||||
const deduped = [...new Set(orderedNames)];
|
const deduped = [...new Set(orderedNames)];
|
||||||
|
|||||||
@@ -229,6 +229,20 @@ function parseKiroIDCFlow(raw: unknown): { flow: KiroIDCFlow; invalid: boolean }
|
|||||||
return { flow: normalizeKiroIDCFlow(normalized), invalid: false };
|
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: {
|
export function getKiroStartIDCValidationError(options: {
|
||||||
kiroMethod: KiroAuthMethod;
|
kiroMethod: KiroAuthMethod;
|
||||||
kiroIDCStartUrl?: string;
|
kiroIDCStartUrl?: string;
|
||||||
@@ -603,6 +617,13 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
const kiroIDCRegion =
|
const kiroIDCRegion =
|
||||||
typeof requestBody.kiroIDCRegion === 'string' ? requestBody.kiroIDCRegion.trim() : undefined;
|
typeof requestBody.kiroIDCRegion === 'string' ? requestBody.kiroIDCRegion.trim() : undefined;
|
||||||
const kiroIDCFlowRaw = requestBody.kiroIDCFlow;
|
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 riskAcknowledgement = requestBody.riskAcknowledgement;
|
||||||
const target = getProxyTarget();
|
const target = getProxyTarget();
|
||||||
if (target.isRemote) {
|
if (target.isRemote) {
|
||||||
@@ -613,6 +634,8 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
const nickname = nicknameRaw?.trim();
|
const nickname = nicknameRaw?.trim();
|
||||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||||
const { flow: kiroIDCFlow, invalid: invalidKiroIDCFlow } = parseKiroIDCFlow(kiroIDCFlowRaw);
|
const { flow: kiroIDCFlow, invalid: invalidKiroIDCFlow } = parseKiroIDCFlow(kiroIDCFlowRaw);
|
||||||
|
const { mode: gitlabAuthMode, invalid: invalidGitLabAuthMode } =
|
||||||
|
parseGitLabAuthMode(gitlabAuthModeRaw);
|
||||||
|
|
||||||
// Validate provider
|
// Validate provider
|
||||||
if (!validProviders.includes(provider as CLIProxyProvider)) {
|
if (!validProviders.includes(provider as CLIProxyProvider)) {
|
||||||
@@ -628,6 +651,14 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider === 'gitlab' && invalidGitLabAuthMode) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Invalid gitlabAuthMode. Supported: oauth, pat',
|
||||||
|
code: 'INVALID_GITLAB_AUTH_MODE',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (provider === 'kiro') {
|
if (provider === 'kiro') {
|
||||||
const kiroIDCValidationError = getKiroStartIDCValidationError({
|
const kiroIDCValidationError = getKiroStartIDCValidationError({
|
||||||
kiroMethod,
|
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 existingAccounts = getProviderAccounts(provider as CLIProxyProvider);
|
||||||
const nicknameError = getStartAuthNicknameError(
|
const nicknameError = getStartAuthNicknameError(
|
||||||
provider as CLIProxyProvider,
|
provider as CLIProxyProvider,
|
||||||
@@ -681,6 +795,8 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
kiroIDCStartUrl: provider === 'kiro' ? kiroIDCStartUrl : undefined,
|
kiroIDCStartUrl: provider === 'kiro' ? kiroIDCStartUrl : undefined,
|
||||||
kiroIDCRegion: provider === 'kiro' ? kiroIDCRegion : undefined,
|
kiroIDCRegion: provider === 'kiro' ? kiroIDCRegion : undefined,
|
||||||
kiroIDCFlow: provider === 'kiro' && kiroMethod === 'idc' ? kiroIDCFlow : 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
|
fromUI: true, // Enable project selection prompt in UI
|
||||||
noIncognito, // Kiro: use normal browser if enabled
|
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>) : {};
|
req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {};
|
||||||
const nicknameRaw = typeof requestBody.nickname === 'string' ? requestBody.nickname : undefined;
|
const nicknameRaw = typeof requestBody.nickname === 'string' ? requestBody.nickname : undefined;
|
||||||
const kiroMethodRaw = requestBody.kiroMethod;
|
const kiroMethodRaw = requestBody.kiroMethod;
|
||||||
|
const gitlabAuthModeRaw = requestBody.gitlabAuthMode;
|
||||||
|
const gitlabBaseUrl =
|
||||||
|
typeof requestBody.gitlabBaseUrl === 'string' ? requestBody.gitlabBaseUrl.trim() : undefined;
|
||||||
const riskAcknowledgement = requestBody.riskAcknowledgement;
|
const riskAcknowledgement = requestBody.riskAcknowledgement;
|
||||||
const nickname = nicknameRaw?.trim();
|
const nickname = nicknameRaw?.trim();
|
||||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||||
|
const { mode: gitlabAuthMode, invalid: invalidGitLabAuthMode } =
|
||||||
|
parseGitLabAuthMode(gitlabAuthModeRaw);
|
||||||
|
|
||||||
// Check remote mode
|
// Check remote mode
|
||||||
const target = getProxyTarget();
|
const target = getProxyTarget();
|
||||||
@@ -862,6 +983,22 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
|||||||
return;
|
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()) {
|
if (provider === 'agy' && !isAntigravityResponsibilityBypassEnabled()) {
|
||||||
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
|
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
@@ -900,11 +1037,18 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
|||||||
provider === 'kiro' && kiroManagementMethod
|
provider === 'kiro' && kiroManagementMethod
|
||||||
? `&method=${encodeURIComponent(kiroManagementMethod)}`
|
? `&method=${encodeURIComponent(kiroManagementMethod)}`
|
||||||
: '';
|
: '';
|
||||||
|
const gitlabQuery =
|
||||||
|
provider === 'gitlab' && gitlabBaseUrl
|
||||||
|
? `&base_url=${encodeURIComponent(gitlabBaseUrl)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
// Call CLIProxyAPI to start OAuth and get auth URL
|
// Call CLIProxyAPI to start OAuth and get auth URL
|
||||||
// CLIProxyAPI management routes are under /v0/management prefix
|
// CLIProxyAPI management routes are under /v0/management prefix
|
||||||
const response = await fetch(
|
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) }
|
{ headers: buildManagementHeaders(target) }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ describe('ProfileDetector', () => {
|
|||||||
expect(result.provider).toBe('gemini');
|
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', () => {
|
it('should detect settings-based profile from unified config', () => {
|
||||||
const settingsPath = path.join(tempDir, 'glm.settings.json');
|
const settingsPath = path.join(tempDir, 'glm.settings.json');
|
||||||
fs.writeFileSync(settingsPath, JSON.stringify({ env: { ANTHROPIC_MODEL: 'glm-4' } }));
|
fs.writeFileSync(settingsPath, JSON.stringify({ env: { ANTHROPIC_MODEL: 'glm-4' } }));
|
||||||
|
|||||||
@@ -121,6 +121,13 @@ describe('Backend Selection', () => {
|
|||||||
assert(types.PLUS_ONLY_PROVIDERS.includes('ghcp'));
|
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', () => {
|
it('does not include gemini as plus-only provider', () => {
|
||||||
assert(!types.PLUS_ONLY_PROVIDERS.includes('gemini'));
|
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',
|
'ghcp',
|
||||||
'claude',
|
'claude',
|
||||||
'kimi',
|
'kimi',
|
||||||
|
'cursor',
|
||||||
|
'gitlab',
|
||||||
|
'codebuddy',
|
||||||
|
'kilo',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validates provider IDs', () => {
|
it('validates provider IDs', () => {
|
||||||
expect(isCLIProxyProvider('gemini')).toBe(true);
|
expect(isCLIProxyProvider('gemini')).toBe(true);
|
||||||
expect(isCLIProxyProvider('ghcp')).toBe(true);
|
expect(isCLIProxyProvider('ghcp')).toBe(true);
|
||||||
|
expect(isCLIProxyProvider('gitlab')).toBe(true);
|
||||||
expect(isCLIProxyProvider('not-a-provider')).toBe(false);
|
expect(isCLIProxyProvider('not-a-provider')).toBe(false);
|
||||||
expect(isCLIProxyProvider('Gemini')).toBe(false);
|
expect(isCLIProxyProvider('Gemini')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns providers by OAuth flow capability', () => {
|
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([
|
expect(getProvidersByOAuthFlow('authorization_code')).toEqual([
|
||||||
'gemini',
|
'gemini',
|
||||||
'codex',
|
'codex',
|
||||||
'agy',
|
'agy',
|
||||||
'iflow',
|
'iflow',
|
||||||
'claude',
|
'claude',
|
||||||
|
'gitlab',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,6 +83,8 @@ describe('provider-capabilities', () => {
|
|||||||
expect(mapExternalProviderName('github-copilot')).toBe('ghcp');
|
expect(mapExternalProviderName('github-copilot')).toBe('ghcp');
|
||||||
expect(mapExternalProviderName('copilot')).toBe('ghcp');
|
expect(mapExternalProviderName('copilot')).toBe('ghcp');
|
||||||
expect(mapExternalProviderName('anthropic')).toBe('claude');
|
expect(mapExternalProviderName('anthropic')).toBe('claude');
|
||||||
|
expect(mapExternalProviderName('gitlab-duo')).toBe('gitlab');
|
||||||
|
expect(mapExternalProviderName('tencent')).toBe('codebuddy');
|
||||||
expect(mapExternalProviderName(' COPILOT ')).toBe('ghcp');
|
expect(mapExternalProviderName(' COPILOT ')).toBe('ghcp');
|
||||||
expect(mapExternalProviderName('')).toBeNull();
|
expect(mapExternalProviderName('')).toBeNull();
|
||||||
expect(mapExternalProviderName('unknown-provider')).toBeNull();
|
expect(mapExternalProviderName('unknown-provider')).toBeNull();
|
||||||
@@ -99,8 +115,11 @@ describe('provider-capabilities', () => {
|
|||||||
it('exposes callback port and display name capabilities', () => {
|
it('exposes callback port and display name capabilities', () => {
|
||||||
expect(getOAuthCallbackPort('qwen')).toBeNull();
|
expect(getOAuthCallbackPort('qwen')).toBeNull();
|
||||||
expect(getOAuthCallbackPort('kiro')).toBeNull();
|
expect(getOAuthCallbackPort('kiro')).toBeNull();
|
||||||
|
expect(getOAuthCallbackPort('cursor')).toBeNull();
|
||||||
|
expect(getOAuthCallbackPort('gitlab')).toBe(17171);
|
||||||
expect(getOAuthCallbackPort('gemini')).toBe(8085);
|
expect(getOAuthCallbackPort('gemini')).toBe(8085);
|
||||||
expect(getProviderDisplayName('agy')).toBe('Antigravity');
|
expect(getProviderDisplayName('agy')).toBe('Antigravity');
|
||||||
|
expect(getProviderDisplayName('kilo')).toBe('Kilo AI');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when provider aliases collide across providers', () => {
|
it('throws when provider aliases collide across providers', () => {
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ describe('completion backend', () => {
|
|||||||
expect(values).toContain('cursor');
|
expect(values).toContain('cursor');
|
||||||
expect(values).toContain('copilot');
|
expect(values).toContain('copilot');
|
||||||
expect(values).toContain('gemini');
|
expect(values).toContain('gemini');
|
||||||
|
expect(values).toContain('gitlab');
|
||||||
|
expect(values).toContain('codebuddy');
|
||||||
|
expect(values).toContain('kilo');
|
||||||
expect(values).toContain('localglm');
|
expect(values).toContain('localglm');
|
||||||
expect(values).toContain('work');
|
expect(values).toContain('work');
|
||||||
expect(values).toContain('my-codex');
|
expect(values).toContain('my-codex');
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ describe('help command parity', () => {
|
|||||||
expect(rendered.includes('gemini')).toBe(true);
|
expect(rendered.includes('gemini')).toBe(true);
|
||||||
expect(rendered.includes('codex')).toBe(true);
|
expect(rendered.includes('codex')).toBe(true);
|
||||||
expect(rendered.includes('ghcp')).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 () => {
|
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('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
|
||||||
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' 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', () => {
|
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('gemini')).toBeNull();
|
||||||
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
|
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
|
||||||
expect(getStartUrlUnsupportedReason('claude')).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', () => {
|
it('keeps generic failure text for other providers', () => {
|
||||||
expect(getStartAuthFailureMessage('gemini')).toBe('Authentication failed or was cancelled');
|
expect(getStartAuthFailureMessage('gemini')).toBe('Authentication failed or was cancelled');
|
||||||
expect(getStartAuthFailureMessage('kiro')).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 [kiroIDCStartUrl, setKiroIDCStartUrl] = useState('');
|
||||||
const [kiroIDCRegion, setKiroIDCRegion] = useState('');
|
const [kiroIDCRegion, setKiroIDCRegion] = useState('');
|
||||||
const [kiroIDCFlow, setKiroIDCFlow] = useState<KiroIDCFlow>(DEFAULT_KIRO_IDC_FLOW);
|
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 { t } = useTranslation();
|
||||||
const wasAuthenticatingRef = useRef(false);
|
const wasAuthenticatingRef = useRef(false);
|
||||||
const powerUserModeRequestIdRef = useRef(0);
|
const powerUserModeRequestIdRef = useRef(0);
|
||||||
@@ -99,6 +102,7 @@ export function AddAccountDialog({
|
|||||||
const kiroImportMutation = useKiroImport();
|
const kiroImportMutation = useKiroImport();
|
||||||
|
|
||||||
const isKiro = provider === 'kiro';
|
const isKiro = provider === 'kiro';
|
||||||
|
const isGitLab = provider === 'gitlab';
|
||||||
const supportsPowerUserMode = provider === 'agy' || provider === 'gemini';
|
const supportsPowerUserMode = provider === 'agy' || provider === 'gemini';
|
||||||
const requiresGeminiSafetyAcknowledgement = provider === 'gemini' && !powerUserModeEnabled;
|
const requiresGeminiSafetyAcknowledgement = provider === 'gemini' && !powerUserModeEnabled;
|
||||||
const requiresAgyResponsibilityFlow = provider === 'agy' && !powerUserModeEnabled;
|
const requiresAgyResponsibilityFlow = provider === 'agy' && !powerUserModeEnabled;
|
||||||
@@ -120,6 +124,8 @@ export function AddAccountDialog({
|
|||||||
const nicknameTrimmed = nickname.trim();
|
const nicknameTrimmed = nickname.trim();
|
||||||
const kiroIDCStartUrlTrimmed = kiroIDCStartUrl.trim();
|
const kiroIDCStartUrlTrimmed = kiroIDCStartUrl.trim();
|
||||||
const kiroIDCRegionTrimmed = kiroIDCRegion.trim();
|
const kiroIDCRegionTrimmed = kiroIDCRegion.trim();
|
||||||
|
const gitlabBaseUrlTrimmed = gitlabBaseUrl.trim();
|
||||||
|
const gitlabPersonalAccessTokenTrimmed = gitlabPersonalAccessToken.trim();
|
||||||
const errorMessage = localError || authFlow.error;
|
const errorMessage = localError || authFlow.error;
|
||||||
|
|
||||||
const fetchPowerUserModeState = useCallback(async (): Promise<boolean> => {
|
const fetchPowerUserModeState = useCallback(async (): Promise<boolean> => {
|
||||||
@@ -191,6 +197,9 @@ export function AddAccountDialog({
|
|||||||
setKiroIDCStartUrl('');
|
setKiroIDCStartUrl('');
|
||||||
setKiroIDCRegion('');
|
setKiroIDCRegion('');
|
||||||
setKiroIDCFlow(DEFAULT_KIRO_IDC_FLOW);
|
setKiroIDCFlow(DEFAULT_KIRO_IDC_FLOW);
|
||||||
|
setGitlabAuthMode('oauth');
|
||||||
|
setGitlabBaseUrl('');
|
||||||
|
setGitlabPersonalAccessToken('');
|
||||||
powerUserModeRequestIdRef.current += 1;
|
powerUserModeRequestIdRef.current += 1;
|
||||||
powerUserModeLoadErrorShownRef.current = false;
|
powerUserModeLoadErrorShownRef.current = false;
|
||||||
wasAuthenticatingRef.current = false;
|
wasAuthenticatingRef.current = false;
|
||||||
@@ -310,6 +319,10 @@ export function AddAccountDialog({
|
|||||||
setLocalError('IDC Start URL is required for Kiro IAM Identity Center login.');
|
setLocalError('IDC Start URL is required for Kiro IAM Identity Center login.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isGitLab && gitlabAuthMode === 'pat' && !gitlabPersonalAccessTokenTrimmed) {
|
||||||
|
setLocalError('GitLab Personal Access Token is required for PAT login.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
wasAuthenticatingRef.current = true;
|
wasAuthenticatingRef.current = true;
|
||||||
authFlow.startAuth(provider, {
|
authFlow.startAuth(provider, {
|
||||||
nickname: nicknameTrimmed || undefined,
|
nickname: nicknameTrimmed || undefined,
|
||||||
@@ -317,8 +330,15 @@ export function AddAccountDialog({
|
|||||||
kiroIDCStartUrl: isKiroIdc ? kiroIDCStartUrlTrimmed : undefined,
|
kiroIDCStartUrl: isKiroIdc ? kiroIDCStartUrlTrimmed : undefined,
|
||||||
kiroIDCRegion: isKiroIdc && kiroIDCRegionTrimmed ? kiroIDCRegionTrimmed : undefined,
|
kiroIDCRegion: isKiroIdc && kiroIDCRegionTrimmed ? kiroIDCRegionTrimmed : undefined,
|
||||||
kiroIDCFlow: isKiroIdc ? kiroIDCFlow : 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,
|
flowType: isKiro ? selectedKiroFlowType : undefined,
|
||||||
startEndpoint: isKiro ? selectedKiroStartEndpoint : undefined,
|
startEndpoint:
|
||||||
|
isKiro ? selectedKiroStartEndpoint : isGitLab && gitlabAuthMode === 'pat' ? 'start' : undefined,
|
||||||
riskAcknowledgement: requiresAgyResponsibilityFlow
|
riskAcknowledgement: requiresAgyResponsibilityFlow
|
||||||
? {
|
? {
|
||||||
version: ANTIGRAVITY_ACK_VERSION,
|
version: ANTIGRAVITY_ACK_VERSION,
|
||||||
@@ -512,6 +532,70 @@ export function AddAccountDialog({
|
|||||||
</div>
|
</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 */}
|
{/* Nickname input - only show before auth starts */}
|
||||||
{!showAuthUI && (
|
{!showAuthUI && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -86,8 +86,12 @@ export function ProviderEditor({
|
|||||||
gemini: ['google'],
|
gemini: ['google'],
|
||||||
agy: ['antigravity'],
|
agy: ['antigravity'],
|
||||||
codex: ['openai'],
|
codex: ['openai'],
|
||||||
|
cursor: ['cursor'],
|
||||||
|
gitlab: ['gitlab', 'duo'],
|
||||||
|
codebuddy: ['codebuddy'],
|
||||||
qwen: ['alibaba', 'qwen'],
|
qwen: ['alibaba', 'qwen'],
|
||||||
iflow: ['iflow'],
|
iflow: ['iflow'],
|
||||||
|
kilo: ['kilo'],
|
||||||
kiro: ['kiro', 'aws'],
|
kiro: ['kiro', 'aws'],
|
||||||
ghcp: ['github', 'copilot'],
|
ghcp: ['github', 'copilot'],
|
||||||
kimi: ['kimi', 'moonshot'],
|
kimi: ['kimi', 'moonshot'],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Constants for Quick Setup Wizard
|
* Constants for Quick Setup Wizard
|
||||||
* Provider display info with custom ordering for wizard UI.
|
* 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';
|
import type { ProviderOption } from './types';
|
||||||
@@ -17,9 +17,13 @@ const WIZARD_PROVIDER_ORDER: CLIProxyProvider[] = [
|
|||||||
'claude',
|
'claude',
|
||||||
'gemini',
|
'gemini',
|
||||||
'codex',
|
'codex',
|
||||||
|
'cursor',
|
||||||
|
'gitlab',
|
||||||
|
'codebuddy',
|
||||||
'qwen',
|
'qwen',
|
||||||
'kimi',
|
'kimi',
|
||||||
'iflow',
|
'iflow',
|
||||||
|
'kilo',
|
||||||
'kiro',
|
'kiro',
|
||||||
'ghcp',
|
'ghcp',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ interface StartAuthOptions {
|
|||||||
kiroIDCStartUrl?: string;
|
kiroIDCStartUrl?: string;
|
||||||
kiroIDCRegion?: string;
|
kiroIDCRegion?: string;
|
||||||
kiroIDCFlow?: 'authcode' | 'device';
|
kiroIDCFlow?: 'authcode' | 'device';
|
||||||
|
gitlabAuthMode?: 'oauth' | 'pat';
|
||||||
|
gitlabBaseUrl?: string;
|
||||||
|
gitlabPersonalAccessToken?: string;
|
||||||
flowType?: 'authorization_code' | 'device_code';
|
flowType?: 'authorization_code' | 'device_code';
|
||||||
startEndpoint?: 'start' | 'start-url';
|
startEndpoint?: 'start' | 'start-url';
|
||||||
riskAcknowledgement?: {
|
riskAcknowledgement?: {
|
||||||
@@ -272,6 +275,9 @@ export function useCliproxyAuthFlow() {
|
|||||||
kiroIDCStartUrl: options?.kiroIDCStartUrl,
|
kiroIDCStartUrl: options?.kiroIDCStartUrl,
|
||||||
kiroIDCRegion: options?.kiroIDCRegion,
|
kiroIDCRegion: options?.kiroIDCRegion,
|
||||||
kiroIDCFlow: options?.kiroIDCFlow,
|
kiroIDCFlow: options?.kiroIDCFlow,
|
||||||
|
gitlabAuthMode: options?.gitlabAuthMode,
|
||||||
|
gitlabBaseUrl: options?.gitlabBaseUrl,
|
||||||
|
gitlabPersonalAccessToken: options?.gitlabPersonalAccessToken,
|
||||||
riskAcknowledgement: options?.riskAcknowledgement,
|
riskAcknowledgement: options?.riskAcknowledgement,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,10 @@ import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/clip
|
|||||||
|
|
||||||
/** Canonical list of CLIProxy provider IDs (shared with backend). */
|
/** Canonical list of CLIProxy provider IDs (shared with backend). */
|
||||||
export const CLIPROXY_PROVIDERS = CLIPROXY_PROVIDER_IDS;
|
export const CLIPROXY_PROVIDERS = CLIPROXY_PROVIDER_IDS;
|
||||||
|
|
||||||
/** Union type for CLIProxy provider IDs */
|
|
||||||
export type CLIProxyProvider = (typeof CLIPROXY_PROVIDERS)[number];
|
export type CLIProxyProvider = (typeof CLIPROXY_PROVIDERS)[number];
|
||||||
export type ProviderVisualId = CLIProxyProvider | 'openai' | 'vertex';
|
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 {
|
export function isValidProvider(provider: string): provider is CLIProxyProvider {
|
||||||
return CLIPROXY_PROVIDERS.includes(provider as CLIProxyProvider);
|
return CLIPROXY_PROVIDERS.includes(provider as CLIProxyProvider);
|
||||||
}
|
}
|
||||||
@@ -37,9 +35,13 @@ interface ProviderMetadata {
|
|||||||
|
|
||||||
const SPECIAL_PROVIDER_VISUAL_IDS = ['openai', 'vertex'] as const;
|
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 {
|
function isProviderVisualId(provider: string): provider is ProviderVisualId {
|
||||||
return (
|
return (
|
||||||
isValidProvider(provider) ||
|
isPresentationProvider(provider) ||
|
||||||
SPECIAL_PROVIDER_VISUAL_IDS.includes(provider as (typeof SPECIAL_PROVIDER_VISUAL_IDS)[number])
|
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',
|
qwen: '/assets/providers/qwen-color.svg',
|
||||||
iflow: '/assets/providers/iflow.png',
|
iflow: '/assets/providers/iflow.png',
|
||||||
kiro: '/assets/providers/kiro.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',
|
ghcp: '/assets/providers/copilot.svg',
|
||||||
claude: '/assets/providers/claude.svg',
|
claude: '/assets/providers/claude.svg',
|
||||||
kimi: '/assets/providers/kimi.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' },
|
qwen: { textClass: 'text-cyan-600', letter: 'Q' },
|
||||||
iflow: { textClass: 'text-indigo-600', letter: 'i' },
|
iflow: { textClass: 'text-indigo-600', letter: 'i' },
|
||||||
kiro: { textClass: 'text-teal-600', letter: 'K' },
|
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' },
|
ghcp: { textClass: 'text-green-600', letter: 'C' },
|
||||||
kimi: { textClass: 'text-orange-500', letter: 'K' },
|
kimi: { textClass: 'text-orange-500', letter: 'K' },
|
||||||
openai: { textClass: 'text-slate-900', letter: 'O' },
|
openai: { textClass: 'text-slate-900', letter: 'O' },
|
||||||
@@ -223,6 +233,10 @@ export const PROVIDER_COLORS: Record<string, string> = {
|
|||||||
iflow: '#f94144',
|
iflow: '#f94144',
|
||||||
qwen: '#6236FF',
|
qwen: '#6236FF',
|
||||||
kiro: '#4d908e',
|
kiro: '#4d908e',
|
||||||
|
cursor: '#111827',
|
||||||
|
gitlab: '#FC6D26',
|
||||||
|
codebuddy: '#2563EB',
|
||||||
|
kilo: '#E11D48',
|
||||||
ghcp: '#43aa8b',
|
ghcp: '#43aa8b',
|
||||||
claude: '#D97757',
|
claude: '#D97757',
|
||||||
kimi: '#FF6B35',
|
kimi: '#FF6B35',
|
||||||
@@ -247,7 +261,7 @@ export function getProviderDisplayName(provider: unknown): string {
|
|||||||
/** Map provider to user-facing short description */
|
/** Map provider to user-facing short description */
|
||||||
export function getProviderDescription(provider: unknown): string {
|
export function getProviderDescription(provider: unknown): string {
|
||||||
const normalized = normalizeProviderInput(provider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
if (!isValidProvider(normalized)) return '';
|
if (!isPresentationProvider(normalized)) return '';
|
||||||
return PROVIDER_METADATA[normalized].description;
|
return PROVIDER_METADATA[normalized].description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,22 @@ describe('AddAccountDialog power user mode', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
fetchMock.mockReset();
|
fetchMock.mockReset();
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
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(() => {
|
afterEach(() => {
|
||||||
@@ -168,4 +184,26 @@ describe('AddAccountDialog power user mode', () => {
|
|||||||
expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument();
|
expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument();
|
||||||
expect(authMocks.startAuth).not.toHaveBeenCalled();
|
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',
|
kiroIDCStartUrl: 'https://d-123.awsapps.com/start',
|
||||||
kiroIDCRegion: 'ca-central-1',
|
kiroIDCRegion: 'ca-central-1',
|
||||||
kiroIDCFlow: 'authcode',
|
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,
|
riskAcknowledgement: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
formatRequestedUpstreamModelRules,
|
formatRequestedUpstreamModelRules,
|
||||||
|
getProviderDescription,
|
||||||
|
getProviderDisplayName,
|
||||||
|
getProviderFallbackVisual,
|
||||||
|
getProviderLogoAsset,
|
||||||
getRequestedUpstreamModelRuleErrors,
|
getRequestedUpstreamModelRuleErrors,
|
||||||
getRequestedModelId,
|
getRequestedModelId,
|
||||||
parseRequestedUpstreamModelRules,
|
parseRequestedUpstreamModelRules,
|
||||||
|
PROVIDER_COLORS,
|
||||||
} from '@/lib/provider-config';
|
} from '@/lib/provider-config';
|
||||||
|
|
||||||
describe('provider model mapping helpers', () => {
|
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