@@ -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,81 @@ const promptForPath = async (): Promise<string> => {
56
62
return promptForPath ( )
57
63
}
58
64
65
+ type IDE = {
66
+ name : string
67
+ command : string
68
+ rulesPath : string
69
+ }
70
+ const IDE : IDE [ ] = [
71
+ {
72
+ name : 'Windsurf' ,
73
+ command : 'windsurf' ,
74
+ rulesPath : IDE_RULES_PATH_MAP . windsurf ,
75
+ } ,
76
+ {
77
+ name : 'Cursor' ,
78
+ command : 'cursor' ,
79
+ rulesPath : IDE_RULES_PATH_MAP . cursor ,
80
+ } ,
81
+ ]
82
+
83
+ /**
84
+ * Checks if a command belongs to a known IDEs by checking if it includes a specific string.
85
+ * For example, the command that starts windsurf looks something like "/applications/windsurf.app/contents/...".
86
+ */
87
+ const getIDEFromCommand = ( command : string ) : IDE | null => {
88
+ // The actual command is something like "/applications/windsurf.app/contents/...", but we are only looking for windsurf
89
+ const match = IDE . find ( ( ide ) => command . includes ( ide . command ) )
90
+ return match ?? null
91
+ }
92
+
93
+ /**
94
+ * Receives a process ID (pid) and returns both the command that the process was run with and its parent process ID. If the process is a known IDE, also returns information about that IDE.
95
+ */
96
+ const getCommandAndParentPID = async (
97
+ pid : number ,
98
+ ) : Promise < {
99
+ parentPID : number
100
+ command : string
101
+ ide : IDE | null
102
+ } > => {
103
+ const { stdout } = await execa ( 'ps' , [ '-p' , String ( pid ) , '-o' , 'ppid=,comm=' ] )
104
+ const output = stdout . trim ( )
105
+ const spaceIndex = output . indexOf ( ' ' )
106
+ const parentPID = output . substring ( 0 , spaceIndex )
107
+ const command = output . substring ( spaceIndex + 1 ) . toLowerCase ( )
108
+ return {
109
+ parentPID : parseInt ( parentPID , 10 ) ,
110
+ command : command ,
111
+ ide : getIDEFromCommand ( command ) ,
112
+ }
113
+ }
114
+
115
+ const getPathByDetectingIDE = async ( ) : Promise < string | null > => {
116
+ // Go up the chain of ancestor process IDs and find if one of their commands matches an IDE.
117
+ const ppid = process . ppid
118
+ let result : Awaited < ReturnType < typeof getCommandAndParentPID > >
119
+ try {
120
+ result = await getCommandAndParentPID ( ppid )
121
+ while ( result . parentPID !== 1 && ! result . ide ) {
122
+ result = await getCommandAndParentPID ( result . parentPID )
123
+ }
124
+ } catch {
125
+ // The command "ps -p {pid} -o ppid=,comm=" didn't work,
126
+ // perhaps we are on a machine that doesn't support it.
127
+ return null
128
+ }
129
+ return result . ide ? result . ide . rulesPath : null
130
+ }
131
+
59
132
export const run = async ( { args, command } : RunRecipeOptions ) => {
60
133
// Start the download in the background while we wait for the prompts.
61
134
const download = downloadFile ( version ) . catch ( ( ) => null )
62
135
63
- const filePath = args [ 0 ] || ( await promptForPath ( ) )
136
+ const filePath =
137
+ args [ 0 ] ||
138
+ ( ( process . env . AI_CONTEXT_SKIP_DETECTION === 'true' ? null : await getPathByDetectingIDE ( ) ) ??
139
+ ( await promptForPath ( ) ) )
64
140
const { contents : downloadedFile , minimumCLIVersion } = ( await download ) ?? { }
65
141
66
142
if ( ! downloadedFile ) {
0 commit comments