fix(cursor): route bare cursor through runtime profile

This commit is contained in:
Tam Nhu Tran
2026-04-01 22:50:58 -04:00
parent 27f6a675be
commit 2d67f40175
19 changed files with 549 additions and 59 deletions
+3 -2
View File
@@ -123,7 +123,7 @@ The dashboard provides visual management for all account types:
| **Gemini** | OAuth | `ccs gemini` | Zero-config, fast iteration |
| **Codex** | OAuth | `ccs codex` | Code generation |
| **Copilot** | OAuth | `ccs copilot` or `ccs ghcp` | GitHub Copilot models |
| **Cursor IDE** | Local Token | `ccs cursor` | Cursor subscription models via local daemon |
| **Cursor IDE** | Local Token | `ccs cursor` | Run Claude through Cursor models via local daemon |
| **Kiro** | OAuth (AWS default) | `ccs kiro` | AWS CodeWhisperer (Claude-powered) |
| **Antigravity** | OAuth | `ccs agy` | Alternative routing |
| **OpenRouter** | API Key | `ccs openrouter` | 300+ models, unified API |
@@ -184,7 +184,7 @@ The dashboard provides visual management for all account types:
ccs # Default Claude session
ccs gemini # Gemini (OAuth)
ccs codex # OpenAI Codex (OAuth)
ccs cursor # Cursor status + local daemon connection details
ccs cursor # Run Claude through Cursor local proxy
ccs kiro # Kiro/AWS CodeWhisperer (OAuth)
ccs ghcp # GitHub Copilot (OAuth device flow)
ccs agy # Antigravity (OAuth)
@@ -379,6 +379,7 @@ Dashboard parity: `ccs config` -> Accounts -> Add Kiro account -> choose `Auth M
ccs cursor enable
ccs cursor auth
ccs cursor start
ccs cursor "explain this repo"
ccs cursor status
```
+11 -5
View File
@@ -43,20 +43,26 @@ ccs cursor auth --manual --token <token> --machine-id <machine-id>
ccs cursor start
```
### 4) Verify status
### 4) Run Cursor-backed Claude
```bash
ccs cursor "explain this repo"
```
### 5) Verify status
```bash
ccs cursor status
```
Or use the bare command to see the same status view plus the local endpoint details
needed for OpenAI-compatible and Anthropic-compatible clients:
Use `ccs cursor` with bare or normal Claude args to run through the local Cursor proxy.
The admin namespace remains available for setup and inspection:
```bash
ccs cursor
ccs cursor help
```
### 5) Stop daemon
### 6) Stop daemon
```bash
ccs cursor stop
+38
View File
@@ -16,6 +16,7 @@ import { Config, Settings, ProfileMetadata } from '../types';
import {
UnifiedConfig,
CopilotConfig,
CursorConfig,
CLIProxyVariantConfig,
CompositeVariantConfig,
CompositeTierConfig,
@@ -49,6 +50,8 @@ export interface ProfileDetectionResult {
env?: Record<string, string>;
/** For copilot profile: the copilot config */
copilotConfig?: CopilotConfig;
/** For cursor profile: the cursor config */
cursorConfig?: CursorConfig;
/** For composite variants: true when variant mixes providers per tier */
isComposite?: boolean;
/** For composite variants: which tier is the default */
@@ -252,6 +255,7 @@ class ProfileDetector {
* 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)
* 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]
@@ -302,6 +306,35 @@ class ProfileDetector {
};
}
// Priority 0.75: Check Cursor profile - local Cursor daemon runtime
if (profileName === 'cursor') {
const unifiedConfig = this.readUnifiedConfig();
const cursorConfig = unifiedConfig?.cursor;
if (!cursorConfig?.enabled) {
const error = new Error(
'Cursor profile is not enabled.\n\n' +
'To enable Cursor integration:\n' +
' 1. Run: ccs cursor enable\n' +
' 2. Import auth: ccs cursor auth\n' +
' 3. Start daemon: ccs cursor start\n\n' +
'Or manually edit ~/.ccs/config.yaml:\n' +
' cursor:\n' +
' enabled: true'
) as ProfileNotFoundError;
error.profileName = profileName;
error.suggestions = [];
error.availableProfiles = this.listAvailableProfiles();
throw error;
}
return {
type: 'cursor',
name: 'cursor',
cursorConfig,
};
}
// Priority 1: Try unified config if available
const unifiedConfig = this.readUnifiedConfig();
if (unifiedConfig) {
@@ -452,6 +485,11 @@ class ProfileDetector {
lines.push(` - copilot (model: ${currentCopilotModel})`);
}
if (unifiedConfig.cursor?.enabled) {
lines.push('Cursor local proxy:');
lines.push(` - cursor (model: ${unifiedConfig.cursor.model})`);
}
// CLIProxy variants from unified config
const variants = Object.keys(unifiedConfig.cliproxy?.variants || {});
if (variants.length > 0) {
+48
View File
@@ -450,6 +450,20 @@ async function main(): Promise<void> {
}
}
// Special case: cursor command (Cursor local proxy integration)
// Route known admin subcommands to the command handler, keep all other args as profile passthrough.
if (firstArg === 'cursor' && args.length > 1) {
const { isCursorSubcommandToken, handleCursorCommand } = await import(
'./commands/cursor-command'
);
const cursorToken = args[1];
if (isCursorSubcommandToken(cursorToken)) {
const exitCode = await handleCursorCommand(args.slice(1));
process.exit(exitCode);
}
}
// First-time install: offer setup wizard for interactive users
// Check independently of recovery status (user may have empty config.yaml)
// Skip if headless, CI, or non-TTY environment
@@ -880,6 +894,40 @@ async function main(): Promise<void> {
claudeCli
);
process.exit(exitCode);
} else if (profileInfo.type === 'cursor') {
// CURSOR FLOW: local Cursor daemon profile
ensureWebSearchMcpOrThrow();
installImageAnalyzerHook();
ensureImageAnalyzerHooks({
profileName: profileInfo.name,
profileType: profileInfo.type,
});
const { executeCursorProfile } = await import('./cursor');
const cursorConfig = profileInfo.cursorConfig;
if (!cursorConfig) {
console.error(fail('Cursor configuration not found'));
process.exit(1);
}
const continuityInheritance = await resolveProfileContinuityInheritance({
profileName: profileInfo.name,
profileType: profileInfo.type,
target: resolvedTarget,
});
if (continuityInheritance.sourceAccount && process.env.CCS_DEBUG) {
console.error(
info(
`Continuity inheritance active: profile "${profileInfo.name}" -> account "${continuityInheritance.sourceAccount}"`
)
);
}
const exitCode = await executeCursorProfile(
cursorConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
);
process.exit(exitCode);
} else if (profileInfo.type === 'settings') {
// Settings-based profiles (glm, glmt) are third-party providers
if (resolvedTarget === 'claude') {
+9 -3
View File
@@ -13,7 +13,7 @@ export function renderCursorHelp(): number {
printLines([
'Cursor IDE Integration',
'',
'Usage: ccs cursor [subcommand] [options]',
'Usage: ccs cursor <subcommand>',
'',
'Subcommands:',
' auth Import Cursor IDE authentication token',
@@ -25,6 +25,9 @@ export function renderCursorHelp(): number {
' disable Disable cursor integration in unified config',
' help Show this help message',
'',
'Runtime entry:',
' ccs cursor [claude args] # Run Claude via the local Cursor proxy',
'',
'Auth options:',
' ccs cursor auth # Auto-detect from Cursor SQLite',
' ccs cursor auth --manual --token <t> --machine-id <id>',
@@ -33,7 +36,8 @@ export function renderCursorHelp(): number {
' 1. ccs cursor enable # Enable integration',
' 2. ccs cursor auth # Import Cursor IDE token',
' 3. ccs cursor start # Start daemon',
' 4. ccs cursor # Show status and runtime connection details',
' 4. ccs cursor "task" # Run Claude through Cursor',
' 5. ccs cursor status # Inspect auth/daemon wiring',
'',
'Or use the web UI: ccs config -> Cursor page',
'',
@@ -93,7 +97,9 @@ export function renderCursorStatus(
console.log('');
console.log('Client setup:');
console.log(` Raw settings: ${dirDisplay}/cursor.settings.json`);
console.log(' Subcommands: ccs cursor help');
console.log(' Runtime entry: ccs cursor [claude args]');
console.log(' Status command: ccs cursor status');
console.log(' Help command: ccs cursor help');
if (isReady) {
return;
+7 -1
View File
@@ -34,6 +34,12 @@ export const CURSOR_SUBCOMMANDS = [
'-h',
] as const;
export function isCursorSubcommandToken(token?: string): boolean {
return (
Boolean(token) && CURSOR_SUBCOMMANDS.includes(token as (typeof CURSOR_SUBCOMMANDS)[number])
);
}
/**
* Handle cursor subcommand.
*/
@@ -56,7 +62,7 @@ export async function handleCursorCommand(args: string[]): Promise<number> {
case 'disable':
return handleDisable();
case undefined:
return handleStatus();
return handleHelp();
case 'help':
case '--help':
case '-h':
+1 -1
View File
@@ -287,7 +287,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
'Auto-detects token from Cursor installation',
],
[
['ccs cursor', 'Cursor status + local daemon connection details'],
['ccs cursor', 'Run Claude via Cursor local proxy'],
['ccs cursor auth', 'Import Cursor token'],
['ccs cursor auth --manual --token <t> --machine-id <id>', 'Manual token import'],
['ccs cursor status', 'Show connection status'],
-7
View File
@@ -172,13 +172,6 @@ const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
await handleSetupCommand(args);
},
},
{
name: 'cursor',
handle: async (args) => {
const { handleCursorCommand } = await import('./cursor-command');
process.exit(await handleCursorCommand(args));
},
},
];
export async function tryHandleRootCommand(args: string[]): Promise<boolean> {
+190
View File
@@ -0,0 +1,190 @@
import { spawn } from 'child_process';
import type { CursorConfig } from '../config/unified-config-types';
import { getGlobalEnvConfig } from '../config/unified-config-loader';
import { ensureCliproxyService } from '../cliproxy';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
import { fail, info, ok } from '../utils/ui';
import {
appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext,
getWebSearchHookEnv,
syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager';
import { getImageAnalysisHookEnv, resolveImageAnalysisRuntimeStatus } from '../utils/hooks';
import { stripClaudeCodeEnv } from '../utils/shell-executor';
import { checkAuthStatus } from './cursor-auth';
import { isDaemonRunning, startDaemon } from './cursor-daemon';
interface CursorImageAnalysisResolution {
env: Record<string, string>;
warning: string | null;
}
export function generateCursorEnv(
config: CursorConfig,
claudeConfigDir?: string
): Record<string, string> {
const opusModel = config.opus_model || config.model;
const sonnetModel = config.sonnet_model || config.model;
const haikuModel = config.haiku_model || config.model;
return {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.port}`,
ANTHROPIC_AUTH_TOKEN: 'cursor-managed',
ANTHROPIC_MODEL: config.model,
ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel,
ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel,
ANTHROPIC_SMALL_FAST_MODEL: haikuModel,
ANTHROPIC_DEFAULT_HAIKU_MODEL: haikuModel,
DISABLE_NON_ESSENTIAL_MODEL_CALLS: '1',
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
...(claudeConfigDir ? { CLAUDE_CONFIG_DIR: claudeConfigDir } : {}),
};
}
export async function resolveCursorImageAnalysisEnv(
verbose = false
): Promise<CursorImageAnalysisResolution> {
const env = getImageAnalysisHookEnv({
profileName: 'cursor',
profileType: 'cursor',
});
const provider = env['CCS_CURRENT_PROVIDER'];
if (env['CCS_IMAGE_ANALYSIS_SKIP'] === '1' || !provider) {
return { env, warning: null };
}
const status = await resolveImageAnalysisRuntimeStatus({
profileName: 'cursor',
profileType: 'cursor',
});
if (status.effectiveRuntimeMode === 'native-read') {
return {
env: {
...env,
CCS_CURRENT_PROVIDER: '',
CCS_IMAGE_ANALYSIS_SKIP: '1',
},
warning: `${status.effectiveRuntimeReason || `Image analysis via ${provider} is unavailable.`} This session will use native Read.`,
};
}
if (status.proxyReadiness === 'stopped') {
const ensureServiceResult = await ensureCliproxyService(CLIPROXY_DEFAULT_PORT, verbose);
if (!ensureServiceResult.started) {
return {
env: {
...env,
CCS_CURRENT_PROVIDER: '',
CCS_IMAGE_ANALYSIS_SKIP: '1',
},
warning: `Image analysis via ${provider} is unavailable because CCS could not start the local CLIProxy service. This session will use native Read.`,
};
}
}
return { env, warning: null };
}
export async function executeCursorProfile(
config: CursorConfig,
claudeArgs: string[],
claudeConfigDir?: string,
claudeCliPath = 'claude'
): Promise<number> {
if (!config.enabled) {
console.error(fail('Cursor integration is not enabled.'));
console.error('');
console.error('Enable it first: ccs cursor enable');
return 1;
}
const authStatus = checkAuthStatus();
if (!authStatus.authenticated) {
console.error(fail('Cursor credentials not found.'));
console.error('');
console.error('Authenticate first: ccs cursor auth');
return 1;
}
if (authStatus.expired) {
console.error(fail('Cursor credentials have expired.'));
console.error('');
console.error('Refresh them with: ccs cursor auth');
return 1;
}
let daemonRunning = await isDaemonRunning(config.port);
if (!daemonRunning) {
if (config.auto_start) {
console.log(info('Starting cursor daemon...'));
const result = await startDaemon({
port: config.port,
ghost_mode: config.ghost_mode,
});
if (!result.success) {
console.error(fail(`Failed to start cursor daemon: ${result.error}`));
return 1;
}
console.log(ok(`Daemon started on port ${config.port}`));
daemonRunning = true;
} else {
console.error(fail('Cursor daemon is not running.'));
console.error('');
console.error('Start the daemon:');
console.error(' ccs cursor start');
console.error('Or enable auto_start in the Cursor config section.');
return 1;
}
}
const cursorEnv = generateCursorEnv(config, claudeConfigDir);
const globalEnvConfig = getGlobalEnvConfig();
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
const webSearchEnv = getWebSearchHookEnv();
const { env: imageAnalysisEnv, warning: imageAnalysisWarning } =
await resolveCursorImageAnalysisEnv();
const env = stripClaudeCodeEnv({
...process.env,
...globalEnv,
...cursorEnv,
...webSearchEnv,
...imageAnalysisEnv,
CCS_PROFILE_TYPE: 'cursor',
});
console.log(info(`Using Cursor proxy (model: ${config.model})`));
if (imageAnalysisWarning) {
console.log(info(imageAnalysisWarning));
}
console.log('');
syncWebSearchMcpToConfigDir(claudeConfigDir);
return new Promise((resolve) => {
const launchArgs = appendThirdPartyWebSearchToolArgs(claudeArgs);
const traceEnv = createWebSearchTraceContext({
launcher: 'cursor.executor',
args: launchArgs,
profile: 'cursor',
profileType: 'cursor',
claudeConfigDir,
});
const proc = spawn(claudeCliPath, launchArgs, {
stdio: 'inherit',
env: { ...env, ...traceEnv },
shell: process.platform === 'win32',
});
proc.on('close', (code) => {
resolve(code ?? 0);
});
proc.on('error', (err) => {
console.error(fail(`Failed to start Claude: ${err.message}`));
resolve(1);
});
});
}
+1
View File
@@ -45,3 +45,4 @@ export {
// Executor
export { CursorExecutor } from './cursor-executor';
export { executeCursorProfile, generateCursorEnv } from './cursor-profile-executor';
+46 -26
View File
@@ -11,6 +11,7 @@ import {
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
import { getProxyTarget } from '../cliproxy/proxy-target-resolver';
import { generateCopilotEnv } from '../copilot/copilot-executor';
import { generateCursorEnv } from '../cursor';
import InstanceManager from '../management/instance-manager';
import SharedManager from '../management/shared-manager';
import { expandPath } from '../utils/helpers';
@@ -97,6 +98,7 @@ function describeProfile(profileName: string, result: ProfileDetectionResult): s
if (result.type === 'account')
return 'Claude account instance isolated through CLAUDE_CONFIG_DIR.';
if (result.type === 'copilot') return 'GitHub Copilot profile routed through copilot-api.';
if (result.type === 'cursor') return 'Cursor profile routed through the local cursor daemon.';
return 'Native Claude profile resolution.';
}
@@ -129,6 +131,12 @@ export function listClaudeExtensionProfiles(): ClaudeExtensionProfileOption[] {
} catch {
// Copilot disabled; skip from setup UI.
}
try {
detector.detectProfileType('cursor');
deduped.push('cursor');
} catch {
// Cursor disabled; skip from setup UI.
}
return deduped
.map((profileName) => createProfileOption(profileName, detector.detectProfileType(profileName)))
@@ -201,40 +209,47 @@ async function resolveExtensionEnv(
}
return generateCopilotEnv(result.copilotConfig, continuity.claudeConfigDir);
})()
: (() => {
if (!result.provider) {
throw new Error(
`Profile "${requestedProfile}" is missing CLIProxy provider metadata.`
);
}
const proxyTarget = getProxyTarget();
const port = result.port || CLIPROXY_DEFAULT_PORT;
if (proxyTarget.isRemote) {
: result.type === 'cursor'
? (() => {
if (!result.cursorConfig) {
throw new Error(`Profile "${requestedProfile}" is missing cursor configuration.`);
}
return generateCursorEnv(result.cursorConfig, continuity.claudeConfigDir);
})()
: (() => {
if (!result.provider) {
throw new Error(
`Profile "${requestedProfile}" is missing CLIProxy provider metadata.`
);
}
const proxyTarget = getProxyTarget();
const port = result.port || CLIPROXY_DEFAULT_PORT;
if (proxyTarget.isRemote) {
warnings.push(
`CLIProxy is configured for remote routing via ${proxyTarget.protocol}://${proxyTarget.host}:${proxyTarget.port}.`
);
return result.isComposite && result.compositeTiers && result.compositeDefaultTier
? getCompositeEnvVars(
result.compositeTiers,
result.compositeDefaultTier,
port,
result.settingsPath,
proxyTarget
)
: getRemoteEnvVars(result.provider, proxyTarget, result.settingsPath);
}
warnings.push(
`CLIProxy is configured for remote routing via ${proxyTarget.protocol}://${proxyTarget.host}:${proxyTarget.port}.`
'CLIProxy-backed profiles require the local or remote proxy endpoint to be reachable.'
);
return result.isComposite && result.compositeTiers && result.compositeDefaultTier
? getCompositeEnvVars(
result.compositeTiers,
result.compositeDefaultTier,
port,
result.settingsPath,
proxyTarget
result.settingsPath
)
: getRemoteEnvVars(result.provider, proxyTarget, result.settingsPath);
}
warnings.push(
'CLIProxy-backed profiles require the local or remote proxy endpoint to be reachable.'
);
return result.isComposite && result.compositeTiers && result.compositeDefaultTier
? getCompositeEnvVars(
result.compositeTiers,
result.compositeDefaultTier,
port,
result.settingsPath
)
: getEffectiveEnvVars(result.provider, port, result.settingsPath);
})();
: getEffectiveEnvVars(result.provider, port, result.settingsPath);
})();
if (result.type === 'settings' && isDeprecatedGlmtProfileName(requestedProfile)) {
const normalized = normalizeDeprecatedGlmtEnv(sortEnvRecord(env));
@@ -257,6 +272,11 @@ async function resolveExtensionEnv(
'copilot-api must stay reachable for this profile to work inside the IDE extension.'
);
}
if (result.type === 'cursor') {
warnings.push(
'The local Cursor daemon must stay reachable for this profile to work inside the IDE extension.'
);
}
if (Object.keys(env).length === 0) {
throw new Error(`Profile "${requestedProfile}" has no extension environment to export.`);
}
@@ -37,6 +37,9 @@ export function evaluateTargetRuntimeCompatibility(
if (input.profileType === 'copilot') {
return unsupported('Factory Droid does not support Copilot profiles.');
}
if (input.profileType === 'cursor') {
return unsupported('Factory Droid does not support Cursor local-proxy profiles.');
}
return { supported: true };
}
@@ -51,6 +54,10 @@ export function evaluateTargetRuntimeCompatibility(
return unsupported('Codex CLI does not support Copilot profiles.');
}
if (input.profileType === 'cursor') {
return unsupported('Codex CLI does not support Cursor local-proxy profiles.');
}
if (input.profileType === 'default') {
return { supported: true };
}
+1 -1
View File
@@ -1,4 +1,4 @@
/**
* Profile mode types used across routing and target adapters.
*/
export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'default';
export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'cursor' | 'default';
+53
View File
@@ -195,5 +195,58 @@ describe('ProfileDetector', () => {
existsSyncSpy.mockRestore();
}
});
it('should detect cursor as a first-class runtime profile when enabled', () => {
const mockUnifiedConfig = {
version: 2,
cursor: {
enabled: true,
port: 20129,
auto_start: true,
ghost_mode: true,
model: 'gpt-5.3-codex',
},
};
const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true);
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(
mockUnifiedConfig as any
);
try {
const result = detector.detectProfileType('cursor');
expect(result.type).toBe('cursor');
expect(result.name).toBe('cursor');
expect(result.cursorConfig?.auto_start).toBe(true);
} finally {
isUnifiedModeSpy.mockRestore();
loadUnifiedConfigSpy.mockRestore();
}
});
it('should throw a helpful error when cursor profile is disabled', () => {
const mockUnifiedConfig = {
version: 2,
cursor: {
enabled: false,
port: 20129,
auto_start: false,
ghost_mode: true,
model: 'gpt-5.3-codex',
},
};
const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true);
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(
mockUnifiedConfig as any
);
try {
expect(() => detector.detectProfileType('cursor')).toThrow(/Cursor profile is not enabled/);
} finally {
isUnifiedModeSpy.mockRestore();
loadUnifiedConfigSpy.mockRestore();
}
});
});
});
@@ -88,13 +88,13 @@ describe('help command parity', () => {
).toBe(true);
});
test('root help documents bare cursor as the status entrypoint', async () => {
test('root help documents bare cursor as the runtime entrypoint', async () => {
const lines: string[] = [];
await handleHelpCommand((line) => lines.push(line));
const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('ccs cursor <cmd>')).toBe(false);
expect(rendered.includes('Cursor status + local daemon connection details')).toBe(true);
expect(rendered.includes('Run Claude via Cursor local proxy')).toBe(true);
});
test('root help explains Claude [1m] as an explicit CCS suffix with upstream limits', async () => {
@@ -74,6 +74,15 @@ describe('root-command-router', () => {
expect(calls).toEqual([]);
});
it('does not capture cursor so bare cursor can fall through to profile routing', async () => {
const tryHandleRootCommand = await loadTryHandleRootCommand();
await expect(tryHandleRootCommand(['cursor'])).resolves.toBe(false);
await expect(tryHandleRootCommand(['cursor', 'status'])).resolves.toBe(false);
expect(calls).toEqual([]);
});
it('prints update help without invoking the updater', async () => {
const tryHandleRootCommand = await loadTryHandleRootCommand();
+6 -10
View File
@@ -303,7 +303,7 @@ describe('stopDaemon', () => {
});
describe('handleCursorCommand', () => {
it('treats bare ccs cursor as a status entrypoint instead of help', async () => {
it('shows help when invoked without an admin subcommand', async () => {
const originalLog = console.log;
const originalError = console.error;
const logs: string[] = [];
@@ -321,10 +321,8 @@ describe('handleCursorCommand', () => {
expect(exitCode).toBe(0);
expect(errors).toHaveLength(0);
expect(logs.some((line) => line.includes('Cursor IDE Status'))).toBe(true);
expect(logs.some((line) => line.includes('Usage: ccs cursor <subcommand> [options]'))).toBe(
false
);
expect(logs.some((line) => line.includes('Cursor IDE Integration'))).toBe(true);
expect(logs.some((line) => line.includes('Usage: ccs cursor <subcommand>'))).toBe(true);
} finally {
console.log = originalLog;
console.error = originalError;
@@ -473,7 +471,7 @@ describe('renderCursorStatus', () => {
});
describe('renderCursorHelp', () => {
it('shows bare ccs cursor as an optional entrypoint', () => {
it('shows bare ccs cursor as the runtime entrypoint', () => {
const originalLog = console.log;
const logs: string[] = [];
@@ -485,11 +483,9 @@ describe('renderCursorHelp', () => {
const exitCode = renderCursorHelp();
expect(exitCode).toBe(0);
expect(logs.some((line) => line.includes('Usage: ccs cursor [subcommand] [options]'))).toBe(
true
);
expect(logs.some((line) => line.includes('Usage: ccs cursor <subcommand>'))).toBe(true);
expect(
logs.some((line) => line.includes('4. ccs cursor # Show status and runtime connection details'))
logs.some((line) => line.includes('ccs cursor [claude args]'))
).toBe(true);
} finally {
console.log = originalLog;
@@ -0,0 +1,89 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
executeCursorProfile,
generateCursorEnv,
} from '../../../src/cursor/cursor-profile-executor';
import { saveCredentials } from '../../../src/cursor/cursor-auth';
import type { CursorConfig } from '../../../src/config/unified-config-types';
const BASE_CONFIG: CursorConfig = {
enabled: true,
port: 20129,
auto_start: false,
ghost_mode: true,
model: 'gpt-5.3-codex',
};
describe('cursor-profile-executor', () => {
let originalCcsHome: string | undefined;
let tempDir: string;
beforeEach(() => {
originalCcsHome = process.env.CCS_HOME;
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cursor-profile-executor-'));
process.env.CCS_HOME = tempDir;
});
afterEach(() => {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
fs.rmSync(tempDir, { recursive: true, force: true });
});
it('builds Cursor env for Claude runtime', () => {
const env = generateCursorEnv(
{
...BASE_CONFIG,
opus_model: 'cursor-opus',
sonnet_model: 'cursor-sonnet',
haiku_model: 'cursor-haiku',
},
'/tmp/claude-config'
);
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:20129');
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('cursor-managed');
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex');
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('cursor-opus');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('cursor-sonnet');
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('cursor-haiku');
expect(env.CLAUDE_CONFIG_DIR).toBe('/tmp/claude-config');
});
it('fails fast when Cursor integration is disabled', async () => {
const exitCode = await executeCursorProfile({ ...BASE_CONFIG, enabled: false }, []);
expect(exitCode).toBe(1);
});
it('fails when credentials are missing', async () => {
const exitCode = await executeCursorProfile(BASE_CONFIG, []);
expect(exitCode).toBe(1);
});
it('fails with actionable guidance when daemon is down and auto_start is false', async () => {
saveCredentials({
accessToken: 'a'.repeat(60),
machineId: '1234567890abcdef1234567890abcdef',
authMethod: 'manual',
importedAt: new Date().toISOString(),
});
const exitCode = await executeCursorProfile(
{
...BASE_CONFIG,
port: 29991,
auto_start: false,
},
[]
);
expect(exitCode).toBe(1);
});
});
@@ -3,6 +3,27 @@ import { describe, expect, test } from 'bun:test';
import { evaluateTargetRuntimeCompatibility } from '../../../src/targets/target-runtime-compatibility';
describe('evaluateTargetRuntimeCompatibility', () => {
test('rejects account, copilot, and cursor profiles on Droid target', () => {
expect(
evaluateTargetRuntimeCompatibility({
target: 'droid',
profileType: 'account',
}).supported
).toBe(false);
expect(
evaluateTargetRuntimeCompatibility({
target: 'droid',
profileType: 'copilot',
}).supported
).toBe(false);
expect(
evaluateTargetRuntimeCompatibility({
target: 'droid',
profileType: 'cursor',
}).supported
).toBe(false);
});
test('supports native Codex default sessions', () => {
expect(
evaluateTargetRuntimeCompatibility({
@@ -69,7 +90,7 @@ describe('evaluateTargetRuntimeCompatibility', () => {
expect(genericSettingsCompatibility.reason).toMatch(/currently supports native default sessions/);
});
test('rejects account and copilot profiles on Codex target', () => {
test('rejects account, copilot, and cursor profiles on Codex target', () => {
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
@@ -82,5 +103,11 @@ describe('evaluateTargetRuntimeCompatibility', () => {
profileType: 'copilot',
}).supported
).toBe(false);
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'cursor',
}).supported
).toBe(false);
});
});