Skip to content

Commit 0fbfd50

Browse files
committed
feat: automatically detects IDE - windsurf or cursor - for AI context files.
1 parent aaa87aa commit 0fbfd50

File tree

1 file changed

+80
-3
lines changed

1 file changed

+80
-3
lines changed

src/recipes/ai-context/index.ts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { resolve } from 'node:path'
22

33
import inquirer from 'inquirer'
44
import semver from 'semver'
5+
import execa from 'execa'
56

67
import type { RunRecipeOptions } from '../../commands/recipes/recipes.js'
78
import { chalk, logAndThrowError, log, version } from '../../utils/command-helpers.js'
@@ -18,9 +19,14 @@ import {
1819

1920
export const description = 'Manage context files for AI tools'
2021

22+
const IDE_RULES_PATH_MAP = {
23+
windsurf: '.windsurf/rules',
24+
cursor: '.cursor/rules',
25+
}
26+
2127
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 },
2430
{ name: 'Custom location', value: '' },
2531
]
2632

@@ -56,11 +62,82 @@ const promptForPath = async (): Promise<string> => {
5662
return promptForPath()
5763
}
5864

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+
59136
export const run = async ({ args, command }: RunRecipeOptions) => {
60137
// Start the download in the background while we wait for the prompts.
61138
const download = downloadFile(version).catch(() => null)
62139

63-
const filePath = args[0] || (await promptForPath())
140+
const filePath = args[0] || ((await getPathByDetectingIDE()) ?? (await promptForPath()))
64141
const { contents: downloadedFile, minimumCLIVersion } = (await download) ?? {}
65142

66143
if (!downloadedFile) {

0 commit comments

Comments
 (0)