Files
automaker/apps/server/src/services/cursor-config-service.ts
Kacper dac916496c feat(server): Implement Cursor CLI permissions management
Added new routes and handlers for managing Cursor CLI permissions, including:
- GET /api/setup/cursor-permissions: Retrieve current permissions configuration and available profiles.
- POST /api/setup/cursor-permissions/profile: Apply a predefined permission profile (global or project).
- POST /api/setup/cursor-permissions/custom: Set custom permissions for a project.
- DELETE /api/setup/cursor-permissions: Delete project-level permissions, reverting to global settings.
- GET /api/setup/cursor-permissions/example: Provide an example config file for a specified profile.

Also introduced a new service for handling Cursor CLI configuration files and updated the UI to support permissions management.

Affected files:
- Added new routes in index.ts and cursor-config.ts
- Created cursor-config-service.ts for permissions management logic
- Updated UI components to display and manage permissions

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-12-30 17:08:18 +01:00

281 lines
7.6 KiB
TypeScript

/**
* Cursor Config Service
*
* Manages Cursor CLI permissions configuration files:
* - Global: ~/.cursor/cli-config.json
* - Project: <project>/.cursor/cli.json
*
* Based on: https://cursor.com/docs/cli/reference/configuration
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import { createLogger } from '@automaker/utils';
import type {
CursorCliConfigFile,
CursorCliPermissions,
CursorPermissionProfile,
} from '@automaker/types';
import {
CURSOR_STRICT_PROFILE,
CURSOR_DEVELOPMENT_PROFILE,
CURSOR_PERMISSION_PROFILES,
} from '@automaker/types';
const logger = createLogger('CursorConfigService');
/**
* Get the path to the global Cursor CLI config
*/
export function getGlobalConfigPath(): string {
// Windows: $env:USERPROFILE\.cursor\cli-config.json
// macOS/Linux: ~/.cursor/cli-config.json
// XDG_CONFIG_HOME override on Linux: $XDG_CONFIG_HOME/cursor/cli-config.json
const xdgConfig = process.env.XDG_CONFIG_HOME;
const cursorConfigDir = process.env.CURSOR_CONFIG_DIR;
if (cursorConfigDir) {
return path.join(cursorConfigDir, 'cli-config.json');
}
if (process.platform === 'linux' && xdgConfig) {
return path.join(xdgConfig, 'cursor', 'cli-config.json');
}
return path.join(os.homedir(), '.cursor', 'cli-config.json');
}
/**
* Get the path to a project's Cursor CLI config
*/
export function getProjectConfigPath(projectPath: string): string {
return path.join(projectPath, '.cursor', 'cli.json');
}
/**
* Read the global Cursor CLI config
*/
export async function readGlobalConfig(): Promise<CursorCliConfigFile | null> {
const configPath = getGlobalConfigPath();
try {
const content = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(content) as CursorCliConfigFile;
logger.debug('Read global Cursor config from:', configPath);
return config;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
logger.debug('Global Cursor config not found at:', configPath);
return null;
}
logger.error('Failed to read global Cursor config:', error);
throw error;
}
}
/**
* Write the global Cursor CLI config
*/
export async function writeGlobalConfig(config: CursorCliConfigFile): Promise<void> {
const configPath = getGlobalConfigPath();
const configDir = path.dirname(configPath);
// Ensure directory exists
await fs.mkdir(configDir, { recursive: true });
// Write config
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
logger.info('Wrote global Cursor config to:', configPath);
}
/**
* Read a project's Cursor CLI config
*/
export async function readProjectConfig(projectPath: string): Promise<CursorCliConfigFile | null> {
const configPath = getProjectConfigPath(projectPath);
try {
const content = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(content) as CursorCliConfigFile;
logger.debug('Read project Cursor config from:', configPath);
return config;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
logger.debug('Project Cursor config not found at:', configPath);
return null;
}
logger.error('Failed to read project Cursor config:', error);
throw error;
}
}
/**
* Write a project's Cursor CLI config
*
* Note: Project-level config ONLY supports permissions.
* The version field and other settings are global-only.
* See: https://cursor.com/docs/cli/reference/configuration
*/
export async function writeProjectConfig(
projectPath: string,
config: CursorCliConfigFile
): Promise<void> {
const configPath = getProjectConfigPath(projectPath);
const configDir = path.dirname(configPath);
// Ensure .cursor directory exists
await fs.mkdir(configDir, { recursive: true });
// Write config (project config ONLY supports permissions - no version field!)
const projectConfig = {
permissions: config.permissions,
};
await fs.writeFile(configPath, JSON.stringify(projectConfig, null, 2));
logger.info('Wrote project Cursor config to:', configPath);
}
/**
* Delete a project's Cursor CLI config
*/
export async function deleteProjectConfig(projectPath: string): Promise<void> {
const configPath = getProjectConfigPath(projectPath);
try {
await fs.unlink(configPath);
logger.info('Deleted project Cursor config:', configPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error;
}
}
}
/**
* Get the effective permissions for a project
* Project config takes precedence over global config
*/
export async function getEffectivePermissions(
projectPath?: string
): Promise<CursorCliPermissions | null> {
// Try project config first
if (projectPath) {
const projectConfig = await readProjectConfig(projectPath);
if (projectConfig?.permissions) {
return projectConfig.permissions;
}
}
// Fall back to global config
const globalConfig = await readGlobalConfig();
return globalConfig?.permissions || null;
}
/**
* Apply a predefined permission profile to a project
*/
export async function applyProfileToProject(
projectPath: string,
profileId: CursorPermissionProfile
): Promise<void> {
const profile = CURSOR_PERMISSION_PROFILES.find((p) => p.id === profileId);
if (!profile) {
throw new Error(`Unknown permission profile: ${profileId}`);
}
await writeProjectConfig(projectPath, {
version: 1,
permissions: profile.permissions,
});
logger.info(`Applied "${profile.name}" profile to project:`, projectPath);
}
/**
* Apply a predefined permission profile globally
*/
export async function applyProfileGlobally(profileId: CursorPermissionProfile): Promise<void> {
const profile = CURSOR_PERMISSION_PROFILES.find((p) => p.id === profileId);
if (!profile) {
throw new Error(`Unknown permission profile: ${profileId}`);
}
// Read existing global config to preserve other settings
const existingConfig = await readGlobalConfig();
await writeGlobalConfig({
version: 1,
...existingConfig,
permissions: profile.permissions,
});
logger.info(`Applied "${profile.name}" profile globally`);
}
/**
* Detect which profile matches the current permissions
*/
export function detectProfile(
permissions: CursorCliPermissions | null
): CursorPermissionProfile | null {
if (!permissions) {
return null;
}
// Check if permissions match a predefined profile
for (const profile of CURSOR_PERMISSION_PROFILES) {
const allowMatch =
JSON.stringify(profile.permissions.allow.sort()) === JSON.stringify(permissions.allow.sort());
const denyMatch =
JSON.stringify(profile.permissions.deny.sort()) === JSON.stringify(permissions.deny.sort());
if (allowMatch && denyMatch) {
return profile.id;
}
}
return 'custom';
}
/**
* Generate example config file content
*/
export function generateExampleConfig(profileId: CursorPermissionProfile = 'development'): string {
const profile =
CURSOR_PERMISSION_PROFILES.find((p) => p.id === profileId) || CURSOR_DEVELOPMENT_PROFILE;
const config: CursorCliConfigFile = {
version: 1,
permissions: profile.permissions,
};
return JSON.stringify(config, null, 2);
}
/**
* Check if a project has Cursor CLI config
*/
export async function hasProjectConfig(projectPath: string): Promise<boolean> {
const configPath = getProjectConfigPath(projectPath);
try {
await fs.access(configPath);
return true;
} catch {
return false;
}
}
/**
* Get all available permission profiles
*/
export function getAvailableProfiles() {
return CURSOR_PERMISSION_PROFILES;
}
// Export profile constants for convenience
export { CURSOR_STRICT_PROFILE, CURSOR_DEVELOPMENT_PROFILE };