mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
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)
This commit is contained in:
@@ -17,6 +17,11 @@ import {
|
|||||||
createGetCursorConfigHandler,
|
createGetCursorConfigHandler,
|
||||||
createSetCursorDefaultModelHandler,
|
createSetCursorDefaultModelHandler,
|
||||||
createSetCursorModelsHandler,
|
createSetCursorModelsHandler,
|
||||||
|
createGetCursorPermissionsHandler,
|
||||||
|
createApplyPermissionProfileHandler,
|
||||||
|
createSetCustomPermissionsHandler,
|
||||||
|
createDeleteProjectPermissionsHandler,
|
||||||
|
createGetExampleConfigHandler,
|
||||||
} from './routes/cursor-config.js';
|
} from './routes/cursor-config.js';
|
||||||
|
|
||||||
export function createSetupRoutes(): Router {
|
export function createSetupRoutes(): Router {
|
||||||
@@ -38,5 +43,12 @@ export function createSetupRoutes(): Router {
|
|||||||
router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler());
|
router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler());
|
||||||
router.post('/cursor-config/models', createSetCursorModelsHandler());
|
router.post('/cursor-config/models', createSetCursorModelsHandler());
|
||||||
|
|
||||||
|
// Cursor CLI Permissions routes
|
||||||
|
router.get('/cursor-permissions', createGetCursorPermissionsHandler());
|
||||||
|
router.post('/cursor-permissions/profile', createApplyPermissionProfileHandler());
|
||||||
|
router.post('/cursor-permissions/custom', createSetCustomPermissionsHandler());
|
||||||
|
router.delete('/cursor-permissions', createDeleteProjectPermissionsHandler());
|
||||||
|
router.get('/cursor-permissions/example', createGetExampleConfigHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,36 @@
|
|||||||
* - GET /api/setup/cursor-config - Get current configuration
|
* - GET /api/setup/cursor-config - Get current configuration
|
||||||
* - POST /api/setup/cursor-config/default-model - Set default model
|
* - POST /api/setup/cursor-config/default-model - Set default model
|
||||||
* - POST /api/setup/cursor-config/models - Set enabled models
|
* - POST /api/setup/cursor-config/models - Set enabled models
|
||||||
|
*
|
||||||
|
* Cursor CLI Permissions endpoints:
|
||||||
|
* - GET /api/setup/cursor-permissions - Get permissions config
|
||||||
|
* - POST /api/setup/cursor-permissions/profile - Apply a permission profile
|
||||||
|
* - POST /api/setup/cursor-permissions/custom - Set custom permissions
|
||||||
|
* - DELETE /api/setup/cursor-permissions - Delete project permissions (use global)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { CursorConfigManager } from '../../../providers/cursor-config-manager.js';
|
import { CursorConfigManager } from '../../../providers/cursor-config-manager.js';
|
||||||
import { CURSOR_MODEL_MAP, type CursorModelId } from '@automaker/types';
|
import {
|
||||||
|
CURSOR_MODEL_MAP,
|
||||||
|
CURSOR_PERMISSION_PROFILES,
|
||||||
|
type CursorModelId,
|
||||||
|
type CursorPermissionProfile,
|
||||||
|
type CursorCliPermissions,
|
||||||
|
} from '@automaker/types';
|
||||||
|
import {
|
||||||
|
readGlobalConfig,
|
||||||
|
readProjectConfig,
|
||||||
|
getEffectivePermissions,
|
||||||
|
applyProfileToProject,
|
||||||
|
applyProfileGlobally,
|
||||||
|
writeProjectConfig,
|
||||||
|
deleteProjectConfig,
|
||||||
|
detectProfile,
|
||||||
|
hasProjectConfig,
|
||||||
|
getAvailableProfiles,
|
||||||
|
generateExampleConfig,
|
||||||
|
} from '../../../services/cursor-config-service.js';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,3 +159,209 @@ export function createSetCursorModelsHandler() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Cursor CLI Permissions Handlers
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates handler for GET /api/setup/cursor-permissions
|
||||||
|
* Returns current permissions configuration and available profiles
|
||||||
|
*/
|
||||||
|
export function createGetCursorPermissionsHandler() {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const projectPath = req.query.projectPath as string | undefined;
|
||||||
|
|
||||||
|
// Get global config
|
||||||
|
const globalConfig = await readGlobalConfig();
|
||||||
|
|
||||||
|
// Get project config if path provided
|
||||||
|
const projectConfig = projectPath ? await readProjectConfig(projectPath) : null;
|
||||||
|
|
||||||
|
// Get effective permissions
|
||||||
|
const effectivePermissions = await getEffectivePermissions(projectPath);
|
||||||
|
|
||||||
|
// Detect which profile is active
|
||||||
|
const activeProfile = detectProfile(effectivePermissions);
|
||||||
|
|
||||||
|
// Check if project has its own config
|
||||||
|
const hasProject = projectPath ? await hasProjectConfig(projectPath) : false;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
globalPermissions: globalConfig?.permissions || null,
|
||||||
|
projectPermissions: projectConfig?.permissions || null,
|
||||||
|
effectivePermissions,
|
||||||
|
activeProfile,
|
||||||
|
hasProjectConfig: hasProject,
|
||||||
|
availableProfiles: getAvailableProfiles(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Get Cursor permissions failed');
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates handler for POST /api/setup/cursor-permissions/profile
|
||||||
|
* Applies a predefined permission profile
|
||||||
|
*/
|
||||||
|
export function createApplyPermissionProfileHandler() {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { profileId, projectPath, scope } = req.body as {
|
||||||
|
profileId: CursorPermissionProfile;
|
||||||
|
projectPath?: string;
|
||||||
|
scope: 'global' | 'project';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate profile
|
||||||
|
const validProfiles = CURSOR_PERMISSION_PROFILES.map((p) => p.id);
|
||||||
|
if (!validProfiles.includes(profileId)) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: `Invalid profile. Valid profiles: ${validProfiles.join(', ')}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === 'project') {
|
||||||
|
if (!projectPath) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'projectPath is required for project scope',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await applyProfileToProject(projectPath, profileId);
|
||||||
|
} else {
|
||||||
|
await applyProfileGlobally(profileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Applied "${profileId}" profile to ${scope}`,
|
||||||
|
scope,
|
||||||
|
profileId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Apply Cursor permission profile failed');
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates handler for POST /api/setup/cursor-permissions/custom
|
||||||
|
* Sets custom permissions for a project
|
||||||
|
*/
|
||||||
|
export function createSetCustomPermissionsHandler() {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { projectPath, permissions } = req.body as {
|
||||||
|
projectPath: string;
|
||||||
|
permissions: CursorCliPermissions;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'projectPath is required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permissions || !Array.isArray(permissions.allow) || !Array.isArray(permissions.deny)) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'permissions must have allow and deny arrays',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeProjectConfig(projectPath, {
|
||||||
|
version: 1,
|
||||||
|
permissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Custom permissions saved',
|
||||||
|
permissions,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Set custom Cursor permissions failed');
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates handler for DELETE /api/setup/cursor-permissions
|
||||||
|
* Deletes project-level permissions (falls back to global)
|
||||||
|
*/
|
||||||
|
export function createDeleteProjectPermissionsHandler() {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const projectPath = req.query.projectPath as string;
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'projectPath query parameter is required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteProjectConfig(projectPath);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Project permissions deleted, using global config',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Delete Cursor project permissions failed');
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates handler for GET /api/setup/cursor-permissions/example
|
||||||
|
* Returns an example config file for a profile
|
||||||
|
*/
|
||||||
|
export function createGetExampleConfigHandler() {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const profileId = (req.query.profileId as CursorPermissionProfile) || 'development';
|
||||||
|
|
||||||
|
const exampleConfig = generateExampleConfig(profileId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
profileId,
|
||||||
|
config: exampleConfig,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Get example Cursor config failed');
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
280
apps/server/src/services/cursor-config-service.ts
Normal file
280
apps/server/src/services/cursor-config-service.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* 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 };
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -9,13 +10,23 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Terminal, Info } from 'lucide-react';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
|
import {
|
||||||
|
Terminal,
|
||||||
|
Info,
|
||||||
|
Shield,
|
||||||
|
ShieldCheck,
|
||||||
|
ShieldAlert,
|
||||||
|
ChevronDown,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { CursorModelId, CursorModelConfig } from '@automaker/types';
|
import type { CursorModelId, CursorModelConfig, CursorPermissionProfile } from '@automaker/types';
|
||||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||||
import {
|
import {
|
||||||
CursorCliStatus,
|
CursorCliStatus,
|
||||||
@@ -30,16 +41,40 @@ interface CursorStatus {
|
|||||||
method?: string;
|
method?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PermissionsData {
|
||||||
|
activeProfile: CursorPermissionProfile | null;
|
||||||
|
effectivePermissions: { allow: string[]; deny: string[] } | null;
|
||||||
|
hasProjectConfig: boolean;
|
||||||
|
availableProfiles: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
permissions: { allow: string[]; deny: string[] };
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export function CursorSettingsTab() {
|
export function CursorSettingsTab() {
|
||||||
// Global settings from store
|
// Global settings from store
|
||||||
const { enabledCursorModels, cursorDefaultModel, setCursorDefaultModel, toggleCursorModel } =
|
const {
|
||||||
useAppStore();
|
enabledCursorModels,
|
||||||
|
cursorDefaultModel,
|
||||||
|
setCursorDefaultModel,
|
||||||
|
toggleCursorModel,
|
||||||
|
currentProject,
|
||||||
|
} = useAppStore();
|
||||||
const { setCursorCliStatus } = useSetupStore();
|
const { setCursorCliStatus } = useSetupStore();
|
||||||
|
|
||||||
const [status, setStatus] = useState<CursorStatus | null>(null);
|
const [status, setStatus] = useState<CursorStatus | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// Permissions state
|
||||||
|
const [permissions, setPermissions] = useState<PermissionsData | null>(null);
|
||||||
|
const [isLoadingPermissions, setIsLoadingPermissions] = useState(false);
|
||||||
|
const [isSavingPermissions, setIsSavingPermissions] = useState(false);
|
||||||
|
const [permissionsExpanded, setPermissionsExpanded] = useState(false);
|
||||||
|
const [copiedConfig, setCopiedConfig] = useState(false);
|
||||||
|
|
||||||
// All available models from the model map
|
// All available models from the model map
|
||||||
const availableModels: CursorModelConfig[] = Object.values(CURSOR_MODEL_MAP);
|
const availableModels: CursorModelConfig[] = Object.values(CURSOR_MODEL_MAP);
|
||||||
|
|
||||||
@@ -105,6 +140,79 @@ export function CursorSettingsTab() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load permissions data
|
||||||
|
const loadPermissions = useCallback(async () => {
|
||||||
|
setIsLoadingPermissions(true);
|
||||||
|
try {
|
||||||
|
const api = getHttpApiClient();
|
||||||
|
const result = await api.setup.getCursorPermissions(currentProject?.path);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setPermissions({
|
||||||
|
activeProfile: result.activeProfile || null,
|
||||||
|
effectivePermissions: result.effectivePermissions || null,
|
||||||
|
hasProjectConfig: result.hasProjectConfig || false,
|
||||||
|
availableProfiles: result.availableProfiles || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load Cursor permissions:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingPermissions(false);
|
||||||
|
}
|
||||||
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
|
// Load permissions when tab is expanded
|
||||||
|
useEffect(() => {
|
||||||
|
if (permissionsExpanded && status?.installed && !permissions) {
|
||||||
|
loadPermissions();
|
||||||
|
}
|
||||||
|
}, [permissionsExpanded, status?.installed, permissions, loadPermissions]);
|
||||||
|
|
||||||
|
// Apply a permission profile
|
||||||
|
const handleApplyProfile = async (
|
||||||
|
profileId: 'strict' | 'development',
|
||||||
|
scope: 'global' | 'project'
|
||||||
|
) => {
|
||||||
|
setIsSavingPermissions(true);
|
||||||
|
try {
|
||||||
|
const api = getHttpApiClient();
|
||||||
|
const result = await api.setup.applyCursorPermissionProfile(
|
||||||
|
profileId,
|
||||||
|
scope,
|
||||||
|
scope === 'project' ? currentProject?.path : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message || `Applied ${profileId} profile`);
|
||||||
|
await loadPermissions();
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to apply profile');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to apply profile');
|
||||||
|
} finally {
|
||||||
|
setIsSavingPermissions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy example config to clipboard
|
||||||
|
const handleCopyConfig = async (profileId: 'strict' | 'development') => {
|
||||||
|
try {
|
||||||
|
const api = getHttpApiClient();
|
||||||
|
const result = await api.setup.getCursorExampleConfig(profileId);
|
||||||
|
|
||||||
|
if (result.success && result.config) {
|
||||||
|
await navigator.clipboard.writeText(result.config);
|
||||||
|
setCopiedConfig(true);
|
||||||
|
toast.success('Config copied to clipboard');
|
||||||
|
setTimeout(() => setCopiedConfig(false), 2000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to copy config');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -238,6 +346,225 @@ export function CursorSettingsTab() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* CLI Permissions Section */}
|
||||||
|
{status?.installed && (
|
||||||
|
<Collapsible open={permissionsExpanded} onOpenChange={setPermissionsExpanded}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger className="w-full">
|
||||||
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-amber-500/20 to-amber-600/10 flex items-center justify-center border border-amber-500/20">
|
||||||
|
<Shield className="w-5 h-5 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||||
|
CLI Permissions
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground/80">
|
||||||
|
Configure what Cursor CLI can do
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{permissions?.activeProfile && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
permissions.activeProfile === 'strict'
|
||||||
|
? 'border-green-500/50 text-green-500'
|
||||||
|
: permissions.activeProfile === 'development'
|
||||||
|
? 'border-blue-500/50 text-blue-500'
|
||||||
|
: 'border-amber-500/50 text-amber-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{permissions.activeProfile === 'strict' && (
|
||||||
|
<ShieldCheck className="w-3 h-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{permissions.activeProfile === 'development' && (
|
||||||
|
<ShieldAlert className="w-3 h-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{permissions.activeProfile}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
'w-5 h-5 text-muted-foreground transition-transform',
|
||||||
|
permissionsExpanded && 'rotate-180'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Security Warning */}
|
||||||
|
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
|
||||||
|
<ShieldAlert className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm text-amber-400/90">
|
||||||
|
<span className="font-medium">Security Notice</span>
|
||||||
|
<p className="text-xs text-amber-400/70 mt-1">
|
||||||
|
Cursor CLI can execute shell commands based on its permission config. For
|
||||||
|
overnight automation, consider using the Strict profile to limit what commands
|
||||||
|
can run.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingPermissions ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="animate-spin w-6 h-6 border-2 border-brand-500 border-t-transparent rounded-full" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Permission Profiles */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>Permission Profiles</Label>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{permissions?.availableProfiles.map((profile) => (
|
||||||
|
<div
|
||||||
|
key={profile.id}
|
||||||
|
className={cn(
|
||||||
|
'p-4 rounded-xl border transition-colors',
|
||||||
|
permissions.activeProfile === profile.id
|
||||||
|
? 'border-brand-500/50 bg-brand-500/5'
|
||||||
|
: 'border-border/50 bg-card/50 hover:bg-accent/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
{profile.id === 'strict' ? (
|
||||||
|
<ShieldCheck className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<ShieldAlert className="w-4 h-4 text-blue-500" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium">{profile.name}</span>
|
||||||
|
{permissions.activeProfile === profile.id && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
{profile.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span className="text-green-500">
|
||||||
|
{profile.permissions.allow.length} allowed
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground/50">|</span>
|
||||||
|
<span className="text-red-500">
|
||||||
|
{profile.permissions.deny.length} denied
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={
|
||||||
|
permissions.activeProfile === profile.id
|
||||||
|
? 'secondary'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
isSavingPermissions || permissions.activeProfile === profile.id
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
handleApplyProfile(
|
||||||
|
profile.id as 'strict' | 'development',
|
||||||
|
'global'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Apply Globally
|
||||||
|
</Button>
|
||||||
|
{currentProject && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isSavingPermissions}
|
||||||
|
onClick={() =>
|
||||||
|
handleApplyProfile(
|
||||||
|
profile.id as 'strict' | 'development',
|
||||||
|
'project'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Apply to Project
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Config File Location */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>Config File Locations</Label>
|
||||||
|
<div className="p-4 rounded-xl border border-border/50 bg-card/30 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Global Config</p>
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
|
~/.cursor/cli-config.json
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleCopyConfig('development')}
|
||||||
|
>
|
||||||
|
{copiedConfig ? (
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-border/30 pt-2">
|
||||||
|
<p className="text-sm font-medium">Project Config</p>
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
|
<project>/.cursor/cli.json
|
||||||
|
</p>
|
||||||
|
{permissions?.hasProjectConfig && (
|
||||||
|
<Badge variant="secondary" className="mt-1 text-xs">
|
||||||
|
Project override active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Documentation Link */}
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Learn more about{' '}
|
||||||
|
<a
|
||||||
|
href="https://cursor.com/docs/cli/reference/permissions"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-brand-500 hover:underline"
|
||||||
|
>
|
||||||
|
Cursor CLI permissions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,6 +562,73 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}> => this.post('/api/setup/cursor-config/models', { projectPath, models }),
|
}> => this.post('/api/setup/cursor-config/models', { projectPath, models }),
|
||||||
|
|
||||||
|
// Cursor CLI Permissions
|
||||||
|
getCursorPermissions: (
|
||||||
|
projectPath?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
globalPermissions?: { allow: string[]; deny: string[] } | null;
|
||||||
|
projectPermissions?: { allow: string[]; deny: string[] } | null;
|
||||||
|
effectivePermissions?: { allow: string[]; deny: string[] } | null;
|
||||||
|
activeProfile?: 'strict' | 'development' | 'custom' | null;
|
||||||
|
hasProjectConfig?: boolean;
|
||||||
|
availableProfiles?: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
permissions: { allow: string[]; deny: string[] };
|
||||||
|
}>;
|
||||||
|
error?: string;
|
||||||
|
}> =>
|
||||||
|
this.get(
|
||||||
|
`/api/setup/cursor-permissions${projectPath ? `?projectPath=${encodeURIComponent(projectPath)}` : ''}`
|
||||||
|
),
|
||||||
|
|
||||||
|
applyCursorPermissionProfile: (
|
||||||
|
profileId: 'strict' | 'development',
|
||||||
|
scope: 'global' | 'project',
|
||||||
|
projectPath?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
scope?: string;
|
||||||
|
profileId?: string;
|
||||||
|
error?: string;
|
||||||
|
}> => this.post('/api/setup/cursor-permissions/profile', { profileId, scope, projectPath }),
|
||||||
|
|
||||||
|
setCursorCustomPermissions: (
|
||||||
|
projectPath: string,
|
||||||
|
permissions: { allow: string[]; deny: string[] }
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
permissions?: { allow: string[]; deny: string[] };
|
||||||
|
error?: string;
|
||||||
|
}> => this.post('/api/setup/cursor-permissions/custom', { projectPath, permissions }),
|
||||||
|
|
||||||
|
deleteCursorProjectPermissions: (
|
||||||
|
projectPath: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}> =>
|
||||||
|
this.httpDelete(
|
||||||
|
`/api/setup/cursor-permissions?projectPath=${encodeURIComponent(projectPath)}`
|
||||||
|
),
|
||||||
|
|
||||||
|
getCursorExampleConfig: (
|
||||||
|
profileId?: 'strict' | 'development'
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
profileId?: string;
|
||||||
|
config?: string;
|
||||||
|
error?: string;
|
||||||
|
}> =>
|
||||||
|
this.get(
|
||||||
|
`/api/setup/cursor-permissions/example${profileId ? `?profileId=${profileId}` : ''}`
|
||||||
|
),
|
||||||
|
|
||||||
onInstallProgress: (callback: (progress: unknown) => void) => {
|
onInstallProgress: (callback: (progress: unknown) => void) => {
|
||||||
return this.subscribeToEvent('agent:stream', callback);
|
return this.subscribeToEvent('agent:stream', callback);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,205 @@ export interface CursorCliConfig {
|
|||||||
rules?: string[]; // .cursor/rules paths
|
rules?: string[]; // .cursor/rules paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Cursor CLI Permissions Configuration
|
||||||
|
// Based on: https://cursor.com/docs/cli/reference/permissions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission string format for Cursor CLI
|
||||||
|
* Examples:
|
||||||
|
* - "Shell(git)" - Allow/deny git commands
|
||||||
|
* - "Shell(npm)" - Allow/deny npm commands
|
||||||
|
* - "Read(.env*)" - Allow/deny reading .env files
|
||||||
|
* - "Write(src/**)" - Allow/deny writing to src directory
|
||||||
|
*/
|
||||||
|
export type CursorPermissionString = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor CLI permissions configuration
|
||||||
|
* Used in ~/.cursor/cli-config.json or <project>/.cursor/cli.json
|
||||||
|
*/
|
||||||
|
export interface CursorCliPermissions {
|
||||||
|
/**
|
||||||
|
* Permitted operations
|
||||||
|
* Format: "Shell(command)", "Read(path)", "Write(path)"
|
||||||
|
*/
|
||||||
|
allow: CursorPermissionString[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forbidden operations (takes precedence over allow)
|
||||||
|
* Format: "Shell(command)", "Read(path)", "Write(path)"
|
||||||
|
*/
|
||||||
|
deny: CursorPermissionString[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full Cursor CLI config file format (cli-config.json / cli.json)
|
||||||
|
* See: https://cursor.com/docs/cli/reference/configuration
|
||||||
|
*/
|
||||||
|
export interface CursorCliConfigFile {
|
||||||
|
/** Schema version (currently 1) */
|
||||||
|
version: 1;
|
||||||
|
|
||||||
|
/** Editor settings (global only) */
|
||||||
|
editor?: {
|
||||||
|
vimMode?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Model settings (global only) */
|
||||||
|
model?: {
|
||||||
|
default?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Permissions (can be project-level) */
|
||||||
|
permissions?: CursorCliPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predefined permission profiles for different use cases
|
||||||
|
*/
|
||||||
|
export type CursorPermissionProfile = 'strict' | 'development' | 'custom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission profile definitions
|
||||||
|
*/
|
||||||
|
export interface CursorPermissionProfileConfig {
|
||||||
|
id: CursorPermissionProfile;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
permissions: CursorCliPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict profile - For read-only operations
|
||||||
|
* Denies all shell commands and writes
|
||||||
|
*/
|
||||||
|
export const CURSOR_STRICT_PROFILE: CursorPermissionProfileConfig = {
|
||||||
|
id: 'strict',
|
||||||
|
name: 'Strict (Read-Only)',
|
||||||
|
description: 'Denies all shell commands and file writes. Safe for analysis tasks.',
|
||||||
|
permissions: {
|
||||||
|
allow: [
|
||||||
|
'Read(**/*)', // Allow reading all files
|
||||||
|
],
|
||||||
|
deny: [
|
||||||
|
'Shell(*)', // Deny all shell commands
|
||||||
|
'Write(**/*)', // Deny all file writes
|
||||||
|
'Read(.env*)', // Deny reading env files
|
||||||
|
'Read(**/*.pem)', // Deny reading private keys
|
||||||
|
'Read(**/*.key)', // Deny reading key files
|
||||||
|
'Read(**/credentials*)', // Deny reading credentials
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Development profile - For feature implementation
|
||||||
|
* Allows safe operations, blocks destructive ones
|
||||||
|
*/
|
||||||
|
export const CURSOR_DEVELOPMENT_PROFILE: CursorPermissionProfileConfig = {
|
||||||
|
id: 'development',
|
||||||
|
name: 'Development',
|
||||||
|
description: 'Allows file edits and safe shell commands. Blocks destructive operations.',
|
||||||
|
permissions: {
|
||||||
|
allow: [
|
||||||
|
'Read(**/*)', // Allow reading all files
|
||||||
|
'Write(**/*)', // Allow writing files
|
||||||
|
'Shell(npm)', // npm install, run, test
|
||||||
|
'Shell(pnpm)', // pnpm install, run, test
|
||||||
|
'Shell(yarn)', // yarn install, run, test
|
||||||
|
'Shell(bun)', // bun install, run, test
|
||||||
|
'Shell(node)', // node scripts
|
||||||
|
'Shell(npx)', // npx commands
|
||||||
|
'Shell(git)', // git operations (except push)
|
||||||
|
'Shell(tsc)', // TypeScript compiler
|
||||||
|
'Shell(eslint)', // Linting
|
||||||
|
'Shell(prettier)', // Formatting
|
||||||
|
'Shell(jest)', // Testing
|
||||||
|
'Shell(vitest)', // Testing
|
||||||
|
'Shell(cargo)', // Rust
|
||||||
|
'Shell(go)', // Go
|
||||||
|
'Shell(python)', // Python
|
||||||
|
'Shell(pip)', // Python packages
|
||||||
|
'Shell(poetry)', // Python packages
|
||||||
|
'Shell(make)', // Makefiles
|
||||||
|
'Shell(docker)', // Docker (build, not run with --rm)
|
||||||
|
'Shell(ls)', // List files
|
||||||
|
'Shell(cat)', // Read files
|
||||||
|
'Shell(echo)', // Echo
|
||||||
|
'Shell(mkdir)', // Create directories
|
||||||
|
'Shell(cp)', // Copy files
|
||||||
|
'Shell(mv)', // Move files
|
||||||
|
'Shell(touch)', // Create files
|
||||||
|
'Shell(pwd)', // Print working directory
|
||||||
|
'Shell(which)', // Find executables
|
||||||
|
'Shell(head)', // Read file head
|
||||||
|
'Shell(tail)', // Read file tail
|
||||||
|
'Shell(grep)', // Search
|
||||||
|
'Shell(find)', // Find files
|
||||||
|
'Shell(wc)', // Word count
|
||||||
|
'Shell(sort)', // Sort
|
||||||
|
'Shell(uniq)', // Unique lines
|
||||||
|
'Shell(diff)', // Diff files
|
||||||
|
'Shell(curl)', // HTTP requests (read-only fetching)
|
||||||
|
'Shell(wget)', // Downloads
|
||||||
|
],
|
||||||
|
deny: [
|
||||||
|
// Destructive file operations
|
||||||
|
'Shell(rm)', // No file deletion
|
||||||
|
'Shell(rmdir)', // No directory deletion
|
||||||
|
'Shell(shred)', // No secure delete
|
||||||
|
|
||||||
|
// Dangerous git operations
|
||||||
|
'Shell(git push)', // No pushing (user should review)
|
||||||
|
'Shell(git push --force)', // Definitely no force push
|
||||||
|
'Shell(git reset --hard)', // No hard reset
|
||||||
|
|
||||||
|
// Package publishing
|
||||||
|
'Shell(npm publish)', // No publishing packages
|
||||||
|
'Shell(pnpm publish)', // No publishing packages
|
||||||
|
'Shell(yarn publish)', // No publishing packages
|
||||||
|
|
||||||
|
// System/network operations
|
||||||
|
'Shell(sudo)', // No sudo
|
||||||
|
'Shell(su)', // No su
|
||||||
|
'Shell(chmod)', // No permission changes
|
||||||
|
'Shell(chown)', // No ownership changes
|
||||||
|
'Shell(kill)', // No process killing
|
||||||
|
'Shell(pkill)', // No process killing
|
||||||
|
'Shell(killall)', // No process killing
|
||||||
|
'Shell(shutdown)', // No shutdown
|
||||||
|
'Shell(reboot)', // No reboot
|
||||||
|
'Shell(systemctl)', // No systemd
|
||||||
|
'Shell(service)', // No services
|
||||||
|
'Shell(iptables)', // No firewall
|
||||||
|
'Shell(ssh)', // No SSH
|
||||||
|
'Shell(scp)', // No SCP
|
||||||
|
|
||||||
|
// Sensitive file access
|
||||||
|
'Read(.env*)', // No reading env files
|
||||||
|
'Read(**/*.pem)', // No reading private keys
|
||||||
|
'Read(**/*.key)', // No reading key files
|
||||||
|
'Read(**/credentials*)', // No reading credentials
|
||||||
|
'Read(**/.git/config)', // No reading git config (may have tokens)
|
||||||
|
'Read(**/id_rsa*)', // No reading SSH keys
|
||||||
|
'Read(**/id_ed25519*)', // No reading SSH keys
|
||||||
|
'Write(.env*)', // No writing env files
|
||||||
|
'Write(**/*.pem)', // No writing keys
|
||||||
|
'Write(**/*.key)', // No writing keys
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All available permission profiles
|
||||||
|
*/
|
||||||
|
export const CURSOR_PERMISSION_PROFILES: CursorPermissionProfileConfig[] = [
|
||||||
|
CURSOR_STRICT_PROFILE,
|
||||||
|
CURSOR_DEVELOPMENT_PROFILE,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cursor authentication status
|
* Cursor authentication status
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user