prjct-cli 0.36.1 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +68 -41
- package/bin/prjct.ts +13 -1
- package/core/agentic/template-executor.ts +14 -4
- package/core/cli/start.ts +14 -4
- package/core/commands/analysis.ts +6 -3
- package/core/commands/setup.ts +27 -17
- package/core/index.ts +45 -26
- package/core/infrastructure/ai-provider.ts +114 -12
- package/core/infrastructure/command-installer.ts +70 -37
- package/core/infrastructure/path-manager.ts +18 -9
- package/core/infrastructure/setup.ts +146 -3
- package/core/types/provider.ts +44 -20
- package/package.json +1 -1
- package/templates/_bases/tracker-base.md +7 -5
- package/templates/commands/github.md +7 -5
- package/templates/commands/init.md +16 -0
- package/templates/commands/jira.md +8 -6
- package/templates/commands/linear.md +8 -6
- package/templates/commands/monday.md +8 -6
- package/templates/commands/sync.md +11 -1
- package/templates/cursor/commands/bug.md +8 -0
- package/templates/cursor/commands/done.md +4 -0
- package/templates/cursor/commands/pause.md +6 -0
- package/templates/cursor/commands/resume.md +4 -0
- package/templates/cursor/commands/ship.md +8 -0
- package/templates/cursor/commands/sync.md +4 -0
- package/templates/cursor/commands/task.md +8 -0
- package/templates/cursor/p.md +29 -0
- package/templates/cursor/router.mdc +28 -0
- package/templates/global/CURSOR.mdc +238 -0
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI Provider - Multi-agent support for prjct-cli
|
|
3
3
|
*
|
|
4
|
-
* Supports
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Supports multiple AI coding agents with a unified abstraction layer:
|
|
5
|
+
* - Claude Code (CLI): ~/.claude/, CLAUDE.md, .md commands
|
|
6
|
+
* - Gemini CLI (CLI): ~/.gemini/, GEMINI.md, .toml commands
|
|
7
|
+
* - Cursor IDE (GUI): .cursor/ (project-level), .mdc rules
|
|
8
|
+
*
|
|
9
|
+
* Key differences:
|
|
10
|
+
* - CLI providers (Claude/Gemini) have global config directories
|
|
11
|
+
* - Cursor has project-level config only (no ~/.cursor/)
|
|
10
12
|
*
|
|
11
13
|
* @see https://geminicli.com/docs/cli/gemini-md/
|
|
12
14
|
* @see https://geminicli.com/docs/cli/skills/
|
|
15
|
+
* @see https://cursor.com/docs/context/rules
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { execSync } from 'child_process'
|
|
@@ -22,6 +25,7 @@ import type {
|
|
|
22
25
|
ProviderDetectionResult,
|
|
23
26
|
ProviderSelectionResult,
|
|
24
27
|
ProviderBranding,
|
|
28
|
+
CursorProjectDetection,
|
|
25
29
|
} from '../types/provider'
|
|
26
30
|
|
|
27
31
|
// =============================================================================
|
|
@@ -66,12 +70,42 @@ export const GeminiProvider: AIProviderConfig = {
|
|
|
66
70
|
docsUrl: 'https://geminicli.com/docs',
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Cursor IDE provider configuration
|
|
75
|
+
*
|
|
76
|
+
* Key differences from Claude/Gemini:
|
|
77
|
+
* - NOT a CLI (GUI app, VS Code fork)
|
|
78
|
+
* - No global config directory (~/.cursor/ doesn't exist)
|
|
79
|
+
* - Project-level config only (.cursor/rules/, .cursor/commands/)
|
|
80
|
+
* - User can select any model (GPT, Claude, Gemini, DeepSeek, etc.)
|
|
81
|
+
*
|
|
82
|
+
* @see https://cursor.com/docs/context/rules
|
|
83
|
+
*/
|
|
84
|
+
export const CursorProvider: AIProviderConfig = {
|
|
85
|
+
name: 'cursor',
|
|
86
|
+
displayName: 'Cursor IDE',
|
|
87
|
+
cliCommand: null, // Not a CLI - GUI app
|
|
88
|
+
configDir: null, // No global config directory
|
|
89
|
+
contextFile: 'prjct.mdc', // Uses .mdc format with frontmatter
|
|
90
|
+
skillsDir: null, // No skills directory
|
|
91
|
+
commandsDir: '.cursor/commands',
|
|
92
|
+
rulesDir: '.cursor/rules', // Cursor-specific: rules directory
|
|
93
|
+
commandFormat: 'md',
|
|
94
|
+
settingsFile: null,
|
|
95
|
+
projectSettingsFile: null,
|
|
96
|
+
ignoreFile: '.cursorignore',
|
|
97
|
+
isProjectLevel: true, // Config is project-level only
|
|
98
|
+
websiteUrl: 'https://cursor.com',
|
|
99
|
+
docsUrl: 'https://cursor.com/docs',
|
|
100
|
+
}
|
|
101
|
+
|
|
69
102
|
/**
|
|
70
103
|
* All available providers
|
|
71
104
|
*/
|
|
72
105
|
export const Providers: Record<AIProviderName, AIProviderConfig> = {
|
|
73
106
|
claude: ClaudeProvider,
|
|
74
107
|
gemini: GeminiProvider,
|
|
108
|
+
cursor: CursorProvider,
|
|
75
109
|
}
|
|
76
110
|
|
|
77
111
|
// =============================================================================
|
|
@@ -105,10 +139,17 @@ function getCliVersion(command: string): string | null {
|
|
|
105
139
|
}
|
|
106
140
|
|
|
107
141
|
/**
|
|
108
|
-
* Detect if a specific provider is installed
|
|
142
|
+
* Detect if a specific CLI-based provider is installed
|
|
143
|
+
* Note: Cursor is NOT a CLI, use detectCursorProject() instead
|
|
109
144
|
*/
|
|
110
145
|
export function detectProvider(provider: AIProviderName): ProviderDetectionResult {
|
|
111
146
|
const config = Providers[provider]
|
|
147
|
+
|
|
148
|
+
// Cursor is not a CLI - return not installed for CLI detection
|
|
149
|
+
if (!config.cliCommand) {
|
|
150
|
+
return { installed: false }
|
|
151
|
+
}
|
|
152
|
+
|
|
112
153
|
const cliPath = whichCommand(config.cliCommand)
|
|
113
154
|
|
|
114
155
|
if (!cliPath) {
|
|
@@ -125,9 +166,10 @@ export function detectProvider(provider: AIProviderName): ProviderDetectionResul
|
|
|
125
166
|
}
|
|
126
167
|
|
|
127
168
|
/**
|
|
128
|
-
* Detect all available providers
|
|
169
|
+
* Detect all available CLI-based providers
|
|
170
|
+
* Note: Cursor detection is project-level, use detectCursorProject() separately
|
|
129
171
|
*/
|
|
130
|
-
export function detectAllProviders():
|
|
172
|
+
export function detectAllProviders(): { claude: ProviderDetectionResult; gemini: ProviderDetectionResult } {
|
|
131
173
|
return {
|
|
132
174
|
claude: detectProvider('claude'),
|
|
133
175
|
gemini: detectProvider('gemini'),
|
|
@@ -165,9 +207,13 @@ export function getActiveProvider(projectProvider?: AIProviderName): AIProviderC
|
|
|
165
207
|
|
|
166
208
|
/**
|
|
167
209
|
* Check if config directory exists for a provider
|
|
210
|
+
* Returns false for project-level providers (Cursor)
|
|
168
211
|
*/
|
|
169
212
|
export function hasProviderConfig(provider: AIProviderName): boolean {
|
|
170
213
|
const config = Providers[provider]
|
|
214
|
+
if (!config.configDir) {
|
|
215
|
+
return false // Cursor has no global config directory
|
|
216
|
+
}
|
|
171
217
|
return fs.existsSync(config.configDir)
|
|
172
218
|
}
|
|
173
219
|
|
|
@@ -189,6 +235,14 @@ Designed for [Gemini](${config.websiteUrl})`,
|
|
|
189
235
|
}
|
|
190
236
|
}
|
|
191
237
|
|
|
238
|
+
if (provider === 'cursor') {
|
|
239
|
+
return {
|
|
240
|
+
commitFooter: `🤖 Generated with [p/](https://www.prjct.app/)
|
|
241
|
+
Built with [Cursor](${config.websiteUrl})`,
|
|
242
|
+
signature: '⚡ prjct + Cursor',
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
192
246
|
// Default: Claude
|
|
193
247
|
return {
|
|
194
248
|
commitFooter: `🤖 Generated with [p/](https://www.prjct.app/)
|
|
@@ -197,30 +251,75 @@ Designed for [Claude](${config.websiteUrl})`,
|
|
|
197
251
|
}
|
|
198
252
|
}
|
|
199
253
|
|
|
254
|
+
// =============================================================================
|
|
255
|
+
// Cursor Project Detection
|
|
256
|
+
// =============================================================================
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Detect if a project is configured for Cursor IDE
|
|
260
|
+
*
|
|
261
|
+
* Cursor has NO global config (~/.cursor/ doesn't exist).
|
|
262
|
+
* Detection is based on project-level .cursor/ directory.
|
|
263
|
+
*/
|
|
264
|
+
export function detectCursorProject(projectRoot: string): CursorProjectDetection {
|
|
265
|
+
const cursorDir = path.join(projectRoot, '.cursor')
|
|
266
|
+
const rulesDir = path.join(cursorDir, 'rules')
|
|
267
|
+
const routerPath = path.join(rulesDir, 'prjct.mdc')
|
|
268
|
+
|
|
269
|
+
const detected = fs.existsSync(cursorDir)
|
|
270
|
+
const routerInstalled = fs.existsSync(routerPath)
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
detected,
|
|
274
|
+
routerInstalled,
|
|
275
|
+
projectRoot: detected ? projectRoot : undefined,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if Cursor routers need to be regenerated
|
|
281
|
+
*/
|
|
282
|
+
export function needsCursorRouterRegeneration(projectRoot: string): boolean {
|
|
283
|
+
const detection = detectCursorProject(projectRoot)
|
|
284
|
+
|
|
285
|
+
// Only check if .cursor/ exists (project uses Cursor)
|
|
286
|
+
// and prjct router is missing
|
|
287
|
+
return detection.detected && !detection.routerInstalled
|
|
288
|
+
}
|
|
289
|
+
|
|
200
290
|
// =============================================================================
|
|
201
291
|
// Provider Paths
|
|
202
292
|
// =============================================================================
|
|
203
293
|
|
|
204
294
|
/**
|
|
205
295
|
* Get full path to global context file
|
|
296
|
+
* Returns null for project-level providers (Cursor)
|
|
206
297
|
*/
|
|
207
|
-
export function getGlobalContextPath(provider: AIProviderName): string {
|
|
298
|
+
export function getGlobalContextPath(provider: AIProviderName): string | null {
|
|
208
299
|
const config = Providers[provider]
|
|
300
|
+
if (!config.configDir) {
|
|
301
|
+
return null // Cursor has no global config
|
|
302
|
+
}
|
|
209
303
|
return path.join(config.configDir, config.contextFile)
|
|
210
304
|
}
|
|
211
305
|
|
|
212
306
|
/**
|
|
213
307
|
* Get full path to global settings file
|
|
308
|
+
* Returns null for project-level providers (Cursor)
|
|
214
309
|
*/
|
|
215
|
-
export function getGlobalSettingsPath(provider: AIProviderName): string {
|
|
310
|
+
export function getGlobalSettingsPath(provider: AIProviderName): string | null {
|
|
216
311
|
const config = Providers[provider]
|
|
312
|
+
if (!config.configDir || !config.settingsFile) {
|
|
313
|
+
return null // Cursor has no global settings
|
|
314
|
+
}
|
|
217
315
|
return path.join(config.configDir, config.settingsFile)
|
|
218
316
|
}
|
|
219
317
|
|
|
220
318
|
/**
|
|
221
319
|
* Get full path to skills directory
|
|
320
|
+
* Returns null for providers without skill support (Cursor)
|
|
222
321
|
*/
|
|
223
|
-
export function getSkillsPath(provider: AIProviderName): string {
|
|
322
|
+
export function getSkillsPath(provider: AIProviderName): string | null {
|
|
224
323
|
return Providers[provider].skillsDir
|
|
225
324
|
}
|
|
226
325
|
|
|
@@ -298,6 +397,7 @@ export default {
|
|
|
298
397
|
Providers,
|
|
299
398
|
ClaudeProvider,
|
|
300
399
|
GeminiProvider,
|
|
400
|
+
CursorProvider,
|
|
301
401
|
detectProvider,
|
|
302
402
|
detectAllProviders,
|
|
303
403
|
getActiveProvider,
|
|
@@ -309,4 +409,6 @@ export default {
|
|
|
309
409
|
getCommandsDir,
|
|
310
410
|
getProjectCommandsPath,
|
|
311
411
|
selectProvider,
|
|
412
|
+
detectCursorProject,
|
|
413
|
+
needsCursorRouterRegeneration,
|
|
312
414
|
}
|
|
@@ -60,32 +60,43 @@ export async function installDocs(): Promise<{ success: boolean; error?: string
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Install or update global CLAUDE.md
|
|
63
|
+
* Install or update global AI agent configuration (CLAUDE.md / GEMINI.md)
|
|
64
64
|
*/
|
|
65
|
-
export async function installGlobalConfig(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
export async function installGlobalConfig(): Promise<GlobalConfigResult> {
|
|
66
|
+
const aiProvider = require('./ai-provider')
|
|
67
|
+
const activeProvider = aiProvider.getActiveProvider()
|
|
68
|
+
const providerName = activeProvider.name
|
|
69
|
+
|
|
70
|
+
// Check if provider is installed
|
|
71
|
+
const detection = aiProvider.detectProvider(providerName)
|
|
72
|
+
if (!detection.installed && !activeProvider.configDir) {
|
|
72
73
|
return {
|
|
73
74
|
success: false,
|
|
74
|
-
error:
|
|
75
|
+
error: `${activeProvider.displayName} not detected`,
|
|
75
76
|
action: 'skipped',
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
try {
|
|
80
|
-
// Ensure
|
|
81
|
-
|
|
82
|
-
await fs.mkdir(claudeDir, { recursive: true })
|
|
81
|
+
// Ensure config directory exists
|
|
82
|
+
await fs.mkdir(activeProvider.configDir, { recursive: true })
|
|
83
83
|
|
|
84
|
-
const globalConfigPath = path.join(
|
|
85
|
-
const templatePath = path.join(getPackageRoot(), 'templates
|
|
84
|
+
const globalConfigPath = path.join(activeProvider.configDir, activeProvider.contextFile)
|
|
85
|
+
const templatePath = path.join(getPackageRoot(), 'templates', 'global', activeProvider.contextFile)
|
|
86
86
|
|
|
87
87
|
// Read template content
|
|
88
|
-
|
|
88
|
+
let templateContent = ''
|
|
89
|
+
try {
|
|
90
|
+
templateContent = await fs.readFile(templatePath, 'utf-8')
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Fallback if provider-specific template not found
|
|
93
|
+
const fallbackTemplatePath = path.join(getPackageRoot(), 'templates/global/CLAUDE.md')
|
|
94
|
+
templateContent = await fs.readFile(fallbackTemplatePath, 'utf-8')
|
|
95
|
+
// If it is Gemini, we should rename Claude to Gemini in the fallback content
|
|
96
|
+
if (providerName === 'gemini') {
|
|
97
|
+
templateContent = templateContent.replace(/Claude/g, 'Gemini')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
89
100
|
|
|
90
101
|
// Check if global config already exists
|
|
91
102
|
let existingContent = ''
|
|
@@ -172,19 +183,26 @@ export class CommandInstaller {
|
|
|
172
183
|
|
|
173
184
|
constructor() {
|
|
174
185
|
this.homeDir = os.homedir()
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
|
|
187
|
+
const aiProvider = require('./ai-provider')
|
|
188
|
+
const activeProvider = aiProvider.getActiveProvider()
|
|
189
|
+
|
|
190
|
+
// Command paths are provider-specific
|
|
191
|
+
if (activeProvider.name === 'gemini') {
|
|
192
|
+
this.claudeCommandsPath = path.join(activeProvider.configDir, 'commands')
|
|
193
|
+
} else {
|
|
194
|
+
// Claude: Commands are in p/ subdirectory to avoid cluttering commands/
|
|
195
|
+
this.claudeCommandsPath = path.join(activeProvider.configDir, 'commands', 'p')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.claudeConfigPath = activeProvider.configDir
|
|
181
199
|
this.templatesDir = path.join(getPackageRoot(), 'templates', 'commands')
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
/**
|
|
185
|
-
* Detect if
|
|
203
|
+
* Detect if active provider is installed
|
|
186
204
|
*/
|
|
187
|
-
async
|
|
205
|
+
async detectActiveProvider(): Promise<boolean> {
|
|
188
206
|
try {
|
|
189
207
|
await fs.access(this.claudeConfigPath)
|
|
190
208
|
return true
|
|
@@ -196,6 +214,13 @@ export class CommandInstaller {
|
|
|
196
214
|
}
|
|
197
215
|
}
|
|
198
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Detect if Claude is installed (legacy support)
|
|
219
|
+
*/
|
|
220
|
+
async detectClaude(): Promise<boolean> {
|
|
221
|
+
return this.detectActiveProvider()
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
/**
|
|
200
225
|
* Get list of command files to install
|
|
201
226
|
*/
|
|
@@ -230,20 +255,22 @@ export class CommandInstaller {
|
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
/**
|
|
233
|
-
* Install commands to
|
|
258
|
+
* Install commands to active AI agent
|
|
234
259
|
*/
|
|
235
260
|
async installCommands(): Promise<InstallResult> {
|
|
236
|
-
const
|
|
261
|
+
const providerDetected = await this.detectActiveProvider()
|
|
262
|
+
const aiProvider = require('./ai-provider')
|
|
263
|
+
const activeProvider = aiProvider.getActiveProvider()
|
|
237
264
|
|
|
238
|
-
if (!
|
|
265
|
+
if (!providerDetected) {
|
|
239
266
|
return {
|
|
240
267
|
success: false,
|
|
241
|
-
error:
|
|
268
|
+
error: `${activeProvider.displayName} not detected. Please install it first.`,
|
|
242
269
|
}
|
|
243
270
|
}
|
|
244
271
|
|
|
245
272
|
try {
|
|
246
|
-
// Install the
|
|
273
|
+
// Install the router to enable "p. task" trigger
|
|
247
274
|
await this.installRouter()
|
|
248
275
|
|
|
249
276
|
// Ensure commands directory exists
|
|
@@ -404,14 +431,20 @@ export class CommandInstaller {
|
|
|
404
431
|
}
|
|
405
432
|
|
|
406
433
|
/**
|
|
407
|
-
* Install the p.md
|
|
434
|
+
* Install the router (p.md for Claude, p.toml for Gemini) to commands directory
|
|
408
435
|
* This enables the "p. task" natural language trigger
|
|
409
|
-
* Claude Code bug #2422 prevents subdirectory slash command discovery
|
|
410
436
|
*/
|
|
411
437
|
async installRouter(): Promise<boolean> {
|
|
438
|
+
const aiProvider = require('./ai-provider')
|
|
439
|
+
const activeProvider = aiProvider.getActiveProvider()
|
|
440
|
+
const routerFile = activeProvider.name === 'gemini' ? 'p.toml' : 'p.md'
|
|
441
|
+
|
|
412
442
|
try {
|
|
413
|
-
const routerSource = path.join(this.templatesDir,
|
|
414
|
-
const routerDest = path.join(
|
|
443
|
+
const routerSource = path.join(this.templatesDir, routerFile)
|
|
444
|
+
const routerDest = path.join(activeProvider.configDir, 'commands', routerFile)
|
|
445
|
+
|
|
446
|
+
// Ensure commands directory exists
|
|
447
|
+
await fs.mkdir(path.dirname(routerDest), { recursive: true })
|
|
415
448
|
|
|
416
449
|
const content = await fs.readFile(routerSource, 'utf-8')
|
|
417
450
|
await fs.writeFile(routerDest, content, 'utf-8')
|
|
@@ -428,12 +461,12 @@ export class CommandInstaller {
|
|
|
428
461
|
* Sync commands - intelligent update that detects and removes orphans
|
|
429
462
|
*/
|
|
430
463
|
async syncCommands(): Promise<SyncResult> {
|
|
431
|
-
const
|
|
464
|
+
const providerDetected = await this.detectActiveProvider()
|
|
432
465
|
|
|
433
|
-
if (!
|
|
466
|
+
if (!providerDetected) {
|
|
434
467
|
return {
|
|
435
468
|
success: false,
|
|
436
|
-
error: '
|
|
469
|
+
error: 'AI agent not detected',
|
|
437
470
|
added: 0,
|
|
438
471
|
updated: 0,
|
|
439
472
|
removed: 0,
|
|
@@ -511,10 +544,10 @@ export class CommandInstaller {
|
|
|
511
544
|
}
|
|
512
545
|
|
|
513
546
|
/**
|
|
514
|
-
* Install or update global CLAUDE.md
|
|
547
|
+
* Install or update global AI agent configuration (CLAUDE.md / GEMINI.md)
|
|
515
548
|
*/
|
|
516
549
|
async installGlobalConfig(): Promise<GlobalConfigResult> {
|
|
517
|
-
return installGlobalConfig(
|
|
550
|
+
return installGlobalConfig()
|
|
518
551
|
}
|
|
519
552
|
|
|
520
553
|
/**
|
|
@@ -301,23 +301,32 @@ class PathManager {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
/**
|
|
304
|
-
* Get the Claude directory path (~/.claude)
|
|
305
|
-
* Contains
|
|
304
|
+
* Get the Claude/Gemini directory path (~/.claude or ~/.gemini)
|
|
305
|
+
* Contains AI CLI configuration
|
|
306
306
|
*/
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
getAgentDir(): string {
|
|
308
|
+
const provider = require('./ai-provider').getActiveProvider()
|
|
309
|
+
return provider.configDir
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get the agent settings file path (~/.claude/settings.json or ~/.gemini/settings.json)
|
|
314
|
+
*/
|
|
315
|
+
getAgentSettingsPath(): string {
|
|
316
|
+
const provider = require('./ai-provider').getActiveProvider()
|
|
317
|
+
const aiProvider = require('./ai-provider')
|
|
318
|
+
return aiProvider.getGlobalSettingsPath(provider.name)
|
|
309
319
|
}
|
|
310
320
|
|
|
311
321
|
/**
|
|
312
|
-
* Get the Claude
|
|
313
|
-
* Contains custom slash commands for Claude
|
|
322
|
+
* Get the Claude directory path (~/.claude) - Legacy support
|
|
314
323
|
*/
|
|
315
|
-
|
|
316
|
-
return path.join(
|
|
324
|
+
getClaudeDir(): string {
|
|
325
|
+
return path.join(os.homedir(), '.claude')
|
|
317
326
|
}
|
|
318
327
|
|
|
319
328
|
/**
|
|
320
|
-
* Get the Claude settings file path (~/.claude/settings.json)
|
|
329
|
+
* Get the Claude settings file path (~/.claude/settings.json) - Legacy support
|
|
321
330
|
*/
|
|
322
331
|
getClaudeSettingsPath(): string {
|
|
323
332
|
return path.join(this.getClaudeDir(), 'settings.json')
|
|
@@ -106,10 +106,11 @@ export async function run(): Promise<SetupResults> {
|
|
|
106
106
|
configAction: null,
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// Step 1: Install for each
|
|
110
|
-
|
|
109
|
+
// Step 1: Install for each CLI-based provider (Claude, Gemini)
|
|
110
|
+
// Note: Cursor is project-level and handled separately via installCursorProject()
|
|
111
|
+
const cliProviderNames: ('claude' | 'gemini')[] = ['claude', 'gemini']
|
|
111
112
|
|
|
112
|
-
for (const providerName of
|
|
113
|
+
for (const providerName of cliProviderNames) {
|
|
113
114
|
const providerConfig = Providers[providerName]
|
|
114
115
|
const providerDetection = detection[providerName]
|
|
115
116
|
|
|
@@ -295,6 +296,148 @@ async function installGeminiGlobalConfig(): Promise<{ success: boolean; action:
|
|
|
295
296
|
}
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
// =============================================================================
|
|
300
|
+
// Cursor IDE Installation (Project-Level)
|
|
301
|
+
// =============================================================================
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Install prjct routers for Cursor IDE in a project
|
|
305
|
+
*
|
|
306
|
+
* Unlike Claude/Gemini which have global config, Cursor uses project-level
|
|
307
|
+
* configuration in .cursor/rules/ and .cursor/commands/.
|
|
308
|
+
*
|
|
309
|
+
* Creates minimal routers that point to the npm package for real instructions.
|
|
310
|
+
* Installs individual command files for better Cursor UX (/sync, /task, etc.)
|
|
311
|
+
*
|
|
312
|
+
* @param projectRoot - The project root directory
|
|
313
|
+
* @returns Object with success status and files created
|
|
314
|
+
*/
|
|
315
|
+
export async function installCursorProject(projectRoot: string): Promise<{
|
|
316
|
+
success: boolean
|
|
317
|
+
rulesCreated: boolean
|
|
318
|
+
commandsCreated: boolean
|
|
319
|
+
gitignoreUpdated: boolean
|
|
320
|
+
}> {
|
|
321
|
+
const result = {
|
|
322
|
+
success: false,
|
|
323
|
+
rulesCreated: false,
|
|
324
|
+
commandsCreated: false,
|
|
325
|
+
gitignoreUpdated: false,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const cursorDir = path.join(projectRoot, '.cursor')
|
|
330
|
+
const rulesDir = path.join(cursorDir, 'rules')
|
|
331
|
+
const commandsDir = path.join(cursorDir, 'commands')
|
|
332
|
+
|
|
333
|
+
const routerMdcDest = path.join(rulesDir, 'prjct.mdc')
|
|
334
|
+
|
|
335
|
+
const routerMdcSource = path.join(getPackageRoot(), 'templates', 'cursor', 'router.mdc')
|
|
336
|
+
const cursorCommandsSource = path.join(getPackageRoot(), 'templates', 'cursor', 'commands')
|
|
337
|
+
|
|
338
|
+
// Ensure directories exist
|
|
339
|
+
fs.mkdirSync(rulesDir, { recursive: true })
|
|
340
|
+
fs.mkdirSync(commandsDir, { recursive: true })
|
|
341
|
+
|
|
342
|
+
// Copy router.mdc → .cursor/rules/prjct.mdc
|
|
343
|
+
if (fs.existsSync(routerMdcSource)) {
|
|
344
|
+
fs.copyFileSync(routerMdcSource, routerMdcDest)
|
|
345
|
+
result.rulesCreated = true
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Copy individual command files → .cursor/commands/
|
|
349
|
+
// This enables /sync, /task, /done, /ship, etc. syntax in Cursor
|
|
350
|
+
if (fs.existsSync(cursorCommandsSource)) {
|
|
351
|
+
const commandFiles = fs.readdirSync(cursorCommandsSource)
|
|
352
|
+
.filter(f => f.endsWith('.md'))
|
|
353
|
+
|
|
354
|
+
for (const file of commandFiles) {
|
|
355
|
+
const src = path.join(cursorCommandsSource, file)
|
|
356
|
+
const dest = path.join(commandsDir, file)
|
|
357
|
+
fs.copyFileSync(src, dest)
|
|
358
|
+
}
|
|
359
|
+
result.commandsCreated = commandFiles.length > 0
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Update .gitignore to exclude prjct Cursor routers
|
|
363
|
+
result.gitignoreUpdated = await addCursorToGitignore(projectRoot)
|
|
364
|
+
|
|
365
|
+
result.success = result.rulesCreated || result.commandsCreated
|
|
366
|
+
return result
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error(`Cursor installation warning: ${(error as Error).message}`)
|
|
369
|
+
return result
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Add Cursor prjct routers to .gitignore
|
|
375
|
+
*
|
|
376
|
+
* These files are per-developer and regenerated automatically.
|
|
377
|
+
*/
|
|
378
|
+
async function addCursorToGitignore(projectRoot: string): Promise<boolean> {
|
|
379
|
+
try {
|
|
380
|
+
const gitignorePath = path.join(projectRoot, '.gitignore')
|
|
381
|
+
const entriesToAdd = [
|
|
382
|
+
'# prjct Cursor routers (regenerated per-developer)',
|
|
383
|
+
'.cursor/rules/prjct.mdc',
|
|
384
|
+
'.cursor/commands/sync.md',
|
|
385
|
+
'.cursor/commands/task.md',
|
|
386
|
+
'.cursor/commands/done.md',
|
|
387
|
+
'.cursor/commands/ship.md',
|
|
388
|
+
'.cursor/commands/bug.md',
|
|
389
|
+
'.cursor/commands/pause.md',
|
|
390
|
+
'.cursor/commands/resume.md',
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
let content = ''
|
|
394
|
+
let fileExists = false
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
content = fs.readFileSync(gitignorePath, 'utf-8')
|
|
398
|
+
fileExists = true
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (!isNotFoundError(error)) {
|
|
401
|
+
throw error
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Check if already added
|
|
406
|
+
if (content.includes('.cursor/rules/prjct.mdc')) {
|
|
407
|
+
return false // Already added
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Append to .gitignore
|
|
411
|
+
const newContent = fileExists
|
|
412
|
+
? content.trimEnd() + '\n\n' + entriesToAdd.join('\n') + '\n'
|
|
413
|
+
: entriesToAdd.join('\n') + '\n'
|
|
414
|
+
|
|
415
|
+
fs.writeFileSync(gitignorePath, newContent, 'utf-8')
|
|
416
|
+
return true
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error(`Gitignore update warning: ${(error as Error).message}`)
|
|
419
|
+
return false
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Check if a project has Cursor configured (has .cursor/ directory)
|
|
425
|
+
*/
|
|
426
|
+
export function hasCursorProject(projectRoot: string): boolean {
|
|
427
|
+
return fs.existsSync(path.join(projectRoot, '.cursor'))
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Check if Cursor routers need regeneration
|
|
432
|
+
*/
|
|
433
|
+
export function needsCursorRegeneration(projectRoot: string): boolean {
|
|
434
|
+
const cursorDir = path.join(projectRoot, '.cursor')
|
|
435
|
+
const routerPath = path.join(cursorDir, 'rules', 'prjct.mdc')
|
|
436
|
+
|
|
437
|
+
// Only check if .cursor/ exists (project uses Cursor)
|
|
438
|
+
return fs.existsSync(cursorDir) && !fs.existsSync(routerPath)
|
|
439
|
+
}
|
|
440
|
+
|
|
298
441
|
/**
|
|
299
442
|
* Migrate existing projects to add cliVersion field
|
|
300
443
|
* This clears the status line warning after npm update
|