diff --git a/src/ccs.ts b/src/ccs.ts index 6442f55a..99249df8 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -65,7 +65,8 @@ import { resolveOfficialChannelsLaunchPlan, } from './channels/official-channels-runtime'; import { getOfficialChannelReadiness } from './channels/official-channels-store'; -import { isCursorSubcommandToken } from './cursor/constants'; +import { isCursorSubcommandToken, shouldUseCursorCliproxyShortcut } from './cursor/constants'; +import { isCLIProxyProvider } from './cliproxy/provider-capabilities'; // Import centralized error handling import { handleError, runCleanup } from './errors'; @@ -123,7 +124,6 @@ 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']); @@ -149,10 +149,6 @@ function detectProfile(args: string[]): DetectedProfile { } } -function shouldUseCursorCliproxyShortcut(args: string[]): boolean { - return args[0] === 'cursor' && CURSOR_CLIPROXY_SHORTCUT_FLAGS.has(args[1] || ''); -} - function resolveRuntimeReasoningFlags( args: string[], envThinkingValue: string | undefined @@ -490,6 +486,18 @@ async function main(): Promise { return; } + if ( + typeof firstArg === 'string' && + isCLIProxyProvider(firstArg) && + firstArg !== 'cursor' && + args.length > 1 && + (args.includes('--help') || args.includes('-h')) + ) { + const { showProviderShortcutHelp } = await import('./commands/help-command'); + await showProviderShortcutHelp(firstArg); + return; + } + // Special case: copilot command (GitHub Copilot integration) // Route known subcommands to command handler, keep all other args as profile passthrough. if (firstArg === 'copilot' && args.length > 1) { diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index e3912a11..e02c00c9 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -1,4 +1,5 @@ import packageJson from '../../package.json'; +import type { CLIProxyProvider } from '../cliproxy'; import { color, dim, header, initUI, subheader } from '../utils/ui'; import { BUILTIN_PROVIDER_SHORTCUTS, @@ -82,10 +83,80 @@ async function showProvidersHelp(writeLine: HelpWriter): Promise { ], writeLine ); + writeCommandTable( + 'GitLab Duo Flags', + [ + { + name: 'ccs gitlab --auth --gitlab-token-login', + summary: 'Authenticate with a GitLab Personal Access Token', + }, + { + name: 'ccs gitlab --auth --token-login', + summary: 'Legacy alias for GitLab PAT login (still supported)', + }, + { + name: 'ccs gitlab --auth --gitlab-url ', + summary: 'Use a self-hosted GitLab base URL during OAuth or PAT auth', + }, + ], + writeLine + ); writeLine(` ${dim('Deep help: ccs cliproxy --help | ccs api --help')}`); writeLine(''); } +export async function showProviderShortcutHelp( + provider: CLIProxyProvider, + writeLine: HelpWriter = console.log +): Promise { + if (provider === 'kiro') { + await showKiroHelp(writeLine); + return; + } + + await initUI(); + + const providerEntry = BUILTIN_PROVIDER_SHORTCUTS.find((entry) => entry.name === provider); + writeLine(header(`CCS ${provider} Shortcut Help`)); + writeLine(''); + writeLine(` ${providerEntry?.summary || 'CLIProxy OAuth provider shortcut'}.`); + writeLine(''); + writeCommandTable( + 'Common Commands', + [ + { name: `ccs ${provider} --auth`, summary: 'Authenticate the provider account via CLIProxy' }, + { name: `ccs ${provider} --accounts`, summary: 'List or manage stored CLIProxy accounts' }, + { name: `ccs ${provider} --config`, summary: 'Open the provider config flow' }, + { name: `ccs ${provider} "task"`, summary: 'Run Claude through this provider shortcut' }, + ], + writeLine + ); + + if (provider === 'gitlab') { + writeCommandTable( + 'GitLab Duo Flags', + [ + { + name: '--gitlab-token-login', + summary: 'Use a GitLab Personal Access Token instead of browser OAuth', + }, + { + name: '--token-login', + summary: 'Legacy alias for `--gitlab-token-login`', + }, + { + name: '--gitlab-url ', + summary: 'Target a self-hosted GitLab base URL', + }, + ], + writeLine + ); + } + + writeLine(` ${dim('See also: ccs help providers | ccs cliproxy --help')}`); + writeLine(''); +} + async function showKiroHelp(writeLine: HelpWriter): Promise { await initUI(); writeLine(header('CCS Kiro Help')); diff --git a/src/cursor/constants.ts b/src/cursor/constants.ts index b9c0d18d..2f1d3c99 100644 --- a/src/cursor/constants.ts +++ b/src/cursor/constants.ts @@ -12,8 +12,35 @@ export const CURSOR_SUBCOMMANDS = [ '-h', ] as const; +export const CURSOR_CLIPROXY_SHORTCUT_FLAGS = new Set([ + '--auth', + '--logout', + '--config', + '--accounts', +]); + export function isCursorSubcommandToken(token?: string): boolean { return ( Boolean(token) && CURSOR_SUBCOMMANDS.includes(token as (typeof CURSOR_SUBCOMMANDS)[number]) ); } + +export function shouldUseCursorCliproxyShortcut(args: string[]): boolean { + if (args[0] !== 'cursor') { + return false; + } + + for (const token of args.slice(1)) { + if (token === '--') { + break; + } + if (CURSOR_CLIPROXY_SHORTCUT_FLAGS.has(token)) { + return true; + } + if (!token.startsWith('-')) { + return false; + } + } + + return false; +} diff --git a/tests/npm/cli.test.js b/tests/npm/cli.test.js index 5d4d20ce..e31a2654 100644 --- a/tests/npm/cli.test.js +++ b/tests/npm/cli.test.js @@ -99,6 +99,20 @@ describe('npm CLI', () => { assert(output.includes('Cursor Live Probe'), 'Should render the cursor probe command output'); } }); + + it('routes gitlab --help to provider shortcut help instead of starting auth', function() { + const output = execSync(`bun "${srcCcsPath}" gitlab --help`, { + encoding: 'utf8', + timeout: 3000, + env: { ...process.env, CCS_HOME: testCcsHome } + }); + + assert(output.includes('CCS gitlab Shortcut Help'), 'Should render provider shortcut help'); + assert(output.includes('--gitlab-token-login'), 'Should document canonical GitLab PAT flag'); + assert(output.includes('--token-login'), 'Should document legacy GitLab PAT alias'); + assert(output.includes('--gitlab-url '), 'Should document self-hosted GitLab URL flag'); + assert(!output.includes('Starting GitLab Duo OAuth'), 'Should not start OAuth when help is requested'); + }); }); describe('Profile handling', () => { diff --git a/tests/unit/commands/help-command-parity.test.ts b/tests/unit/commands/help-command-parity.test.ts index 4d3864f7..d8563c49 100644 --- a/tests/unit/commands/help-command-parity.test.ts +++ b/tests/unit/commands/help-command-parity.test.ts @@ -53,6 +53,9 @@ describe('help command parity', () => { expect(rendered.includes('gitlab')).toBe(true); expect(rendered.includes('codebuddy')).toBe(true); expect(rendered.includes('kilo')).toBe(true); + expect(rendered.includes('--gitlab-token-login')).toBe(true); + expect(rendered.includes('--token-login')).toBe(true); + expect(rendered.includes('--gitlab-url ')).toBe(true); }); test('kiro topic documents IDC and callback flags', async () => { diff --git a/tests/unit/cursor/cursor-shortcut-routing.test.ts b/tests/unit/cursor/cursor-shortcut-routing.test.ts new file mode 100644 index 00000000..5b50ca51 --- /dev/null +++ b/tests/unit/cursor/cursor-shortcut-routing.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'bun:test'; +import { shouldUseCursorCliproxyShortcut } from '../../../src/cursor/constants'; + +describe('cursor CLIProxy shortcut routing', () => { + it('accepts shortcut flags after leading generic flags', () => { + expect(shouldUseCursorCliproxyShortcut(['cursor', '--auth'])).toBe(true); + expect(shouldUseCursorCliproxyShortcut(['cursor', '--verbose', '--auth'])).toBe(true); + expect(shouldUseCursorCliproxyShortcut(['cursor', '-v', '--accounts'])).toBe(true); + }); + + it('stops scanning once a positional legacy runtime argument appears', () => { + expect(shouldUseCursorCliproxyShortcut(['cursor', 'write', '--auth'])).toBe(false); + expect(shouldUseCursorCliproxyShortcut(['cursor', 'status'])).toBe(false); + }); +});