@@ -2,6 +2,7 @@ import { resolve } from 'node:path'
2
2
3
3
import inquirer from 'inquirer'
4
4
import semver from 'semver'
5
+ import execa from 'execa'
5
6
6
7
import type { RunRecipeOptions } from '../../commands/recipes/recipes.js'
7
8
import { chalk , logAndThrowError , log , version } from '../../utils/command-helpers.js'
@@ -18,9 +19,14 @@ import {
18
19
19
20
export const description = 'Manage context files for AI tools'
20
21
22
+ const IDE_RULES_PATH_MAP = {
23
+ windsurf : '.windsurf/rules' ,
24
+ cursor : '.cursor/rules' ,
25
+ }
26
+
21
27
const presets = [
22
- { name : 'Windsurf rules (.windsurf/rules/)' , value : ' .windsurf/rules' } ,
23
- { name : 'Cursor rules (.cursor/rules/)' , value : ' .cursor/rules' } ,
28
+ { name : 'Windsurf rules (.windsurf/rules/)' , value : IDE_RULES_PATH_MAP . windsurf } ,
29
+ { name : 'Cursor rules (.cursor/rules/)' , value : IDE_RULES_PATH_MAP . cursor } ,
24
30
{ name : 'Custom location' , value : '' } ,
25
31
]
26
32
@@ -56,11 +62,82 @@ const promptForPath = async (): Promise<string> => {
56
62
return promptForPath ( )
57
63
}
58
64
65
+ type IDE = {
66
+ name : string
67
+ command : string
68
+ path : string
69
+ }
70
+ const IDE : IDE [ ] = [
71
+ {
72
+ name : 'Windsurf' ,
73
+ command : 'windsurf' ,
74
+ path : IDE_RULES_PATH_MAP . windsurf ,
75
+ } ,
76
+ {
77
+ name : 'Cursor' ,
78
+ command : 'cursor' ,
79
+ path : IDE_RULES_PATH_MAP . cursor ,
80
+ } ,
81
+ ]
82
+
83
+ const getPathByDetectingIDE = async ( ) : Promise < string | null > => {
84
+ const getIDEFromCommand = ( command : string ) : IDE | null => {
85
+ const match = IDE . find ( ( ide ) => command . includes ( ide . command ) )
86
+ return match ?? null
87
+ }
88
+
89
+ async function getCommandAndParentPID ( pid : number ) : Promise < {
90
+ parentPID : number
91
+ command : string
92
+ ide : IDE | null
93
+ } > {
94
+ const { stdout } = await execa ( 'ps' , [ '-p' , String ( pid ) , '-o' , 'ppid=,comm=' ] )
95
+ const output = stdout . trim ( )
96
+ const spaceIndex = output . indexOf ( ' ' )
97
+ const parentPID = output . substring ( 0 , spaceIndex )
98
+ const command = output . substring ( spaceIndex + 1 ) . toLowerCase ( )
99
+ return {
100
+ parentPID : parseInt ( parentPID , 10 ) ,
101
+ command : command ,
102
+ ide : getIDEFromCommand ( command ) ,
103
+ }
104
+ }
105
+
106
+ // Go up the chain of ancestor process IDs and find if one of their commands matches an IDE.
107
+ const ppid = process . ppid
108
+ let result : Awaited < ReturnType < typeof getCommandAndParentPID > >
109
+ try {
110
+ result = await getCommandAndParentPID ( ppid )
111
+ while ( result . parentPID !== 1 && ! result . ide ) {
112
+ result = await getCommandAndParentPID ( result . parentPID )
113
+ }
114
+ } catch ( _ ) {
115
+ // The command "ps -p {pid} -o ppid=,comm=" didn't work,
116
+ // perhaps we are on a machine that doesn't support it.
117
+ return null
118
+ }
119
+
120
+ if ( result . ide ) {
121
+ const { saveToPath } = await inquirer . prompt ( [
122
+ {
123
+ name : 'saveToPath' ,
124
+ message : `We detected that you're using ${ result . ide . name } . Would you like us to store the context files in ${ result . ide . path } ?` ,
125
+ type : 'confirm' ,
126
+ default : true ,
127
+ } ,
128
+ ] )
129
+ if ( saveToPath ) {
130
+ return result . ide . path
131
+ }
132
+ }
133
+ return null
134
+ }
135
+
59
136
export const run = async ( { args, command } : RunRecipeOptions ) => {
60
137
// Start the download in the background while we wait for the prompts.
61
138
const download = downloadFile ( version ) . catch ( ( ) => null )
62
139
63
- const filePath = args [ 0 ] || ( await promptForPath ( ) )
140
+ const filePath = args [ 0 ] || ( ( await getPathByDetectingIDE ( ) ) ?? ( await promptForPath ( ) ) )
64
141
const { contents : downloadedFile , minimumCLIVersion } = ( await download ) ?? { }
65
142
66
143
if ( ! downloadedFile ) {
0 commit comments