Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pr-1079.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sanity/cli': minor
---

Configure Sanity MCP and install the `sanity-best-practices` agent skill for detected AI editors in a single step during `sanity init`. Add `--no-skills` to opt out.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@commitlint/types": "^20.4.0",
"@eslint/compat": "catalog:",
"@sanity/eslint-config-cli": "workspace:*",
"@vercel/detect-agent": "^1.2.3",
"@vitest/coverage-istanbul": "catalog:",
"eslint": "catalog:",
"husky": "^9.1.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ describe('#getViteConfig', () => {
test('should create basic vite config with default options', async () => {
const options = {
cwd: mockTestCwd,
mode: 'development' as const,
reactCompiler: undefined,
getEnvironmentVariables() {
return {'process.env.STUDIO_VAR': '"studio-value"'}
},
mode: 'development' as const,
reactCompiler: undefined,
}

const config = await getViteConfig(options)
Expand Down Expand Up @@ -142,12 +142,12 @@ describe('#getViteConfig', () => {
test('should create vite config for app mode', async () => {
const options = {
cwd: mockTestCwd,
isApp: true,
mode: 'development' as const,
reactCompiler: undefined,
getEnvironmentVariables() {
return {'process.env.APP_VAR': '"app-value"'}
},
isApp: true,
mode: 'development' as const,
reactCompiler: undefined,
}

const config = await getViteConfig(options)
Expand All @@ -168,12 +168,12 @@ describe('#getViteConfig', () => {
test('should create production config with minification', async () => {
const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
minify: true,
mode: 'production' as const,
outputDir: mockCustomOutput,
reactCompiler: undefined,
sourceMap: false,
getEnvironmentVariables,
}

const config = await getViteConfig(options)
Expand All @@ -199,10 +199,10 @@ describe('#getViteConfig', () => {
test('should create production config without minification', async () => {
const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
minify: false,
mode: 'production' as const,
reactCompiler: undefined,
getEnvironmentVariables,
}

const config = await getViteConfig(options)
Expand All @@ -216,9 +216,9 @@ describe('#getViteConfig', () => {
const options = {
basePath: 'custom/path',
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'development' as const,
reactCompiler: undefined,
getEnvironmentVariables,
}

await getViteConfig(options)
Expand All @@ -229,13 +229,13 @@ describe('#getViteConfig', () => {
test('should handle custom server options', async () => {
const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'development' as const,
reactCompiler: undefined,
server: {
host: '0.0.0.0',
port: 8080,
},
getEnvironmentVariables,
}

const config = await getViteConfig(options)
Expand All @@ -257,9 +257,9 @@ describe('#getViteConfig', () => {

const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'development' as const,
reactCompiler: reactCompilerConfig,
getEnvironmentVariables,
}

await getViteConfig(options)
Expand All @@ -279,9 +279,9 @@ describe('#getViteConfig', () => {

const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'development' as const,
reactCompiler: undefined,
getEnvironmentVariables,
}

const config = await getViteConfig(options)
Expand All @@ -299,10 +299,10 @@ describe('#getViteConfig', () => {

const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
importMap,
mode: 'production' as const,
reactCompiler: undefined,
getEnvironmentVariables,
}

const {createExternalFromImportMap} = await import('../createExternalFromImportMap.js')
Expand All @@ -325,9 +325,9 @@ describe('#getViteConfig', () => {
const options = {
basePath: '/studio',
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'development' as const,
reactCompiler: undefined,
getEnvironmentVariables,
}

await getViteConfig(options)
Expand All @@ -342,6 +342,7 @@ describe('#getViteConfig', () => {
test('should include schema extraction plugin when enabled', async () => {
const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'development' as const,
reactCompiler: undefined,
schemaExtraction: {
Expand All @@ -351,7 +352,6 @@ describe('#getViteConfig', () => {
watchPatterns: ['custom/**/*.ts'],
workspace: 'production',
},
getEnvironmentVariables,
}

const config = await getViteConfig(options)
Expand All @@ -375,13 +375,13 @@ describe('#getViteConfig', () => {
test('should not include schema extraction plugin when disabled', async () => {
const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'development' as const,
reactCompiler: undefined,
schemaExtraction: {
enabled: false,
path: 'schema.json',
},
getEnvironmentVariables,
}

const config = await getViteConfig(options)
Expand All @@ -402,9 +402,9 @@ describe('#getViteConfig', () => {
},
],
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'development' as const,
reactCompiler: undefined,
getEnvironmentVariables,
}

const config = await getViteConfig(options)
Expand Down Expand Up @@ -585,9 +585,9 @@ describe('#onRollupWarn and #suppressUnusedImport helper functions', () => {
// which includes the onwarn callback
const options = {
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'production' as const,
reactCompiler: undefined,
getEnvironmentVariables,
}

const config = await getViteConfig(options)
Expand Down Expand Up @@ -615,9 +615,9 @@ describe('#onRollupWarn and #suppressUnusedImport helper functions', () => {

const config = await getViteConfig({
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'production' as const,
reactCompiler: undefined,
getEnvironmentVariables,
})

const onwarn = config.build?.rollupOptions?.onwarn
Expand All @@ -638,9 +638,9 @@ describe('#onRollupWarn and #suppressUnusedImport helper functions', () => {

const config = await getViteConfig({
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'production' as const,
reactCompiler: undefined,
getEnvironmentVariables,
})

const onwarn = config.build?.rollupOptions?.onwarn
Expand All @@ -660,9 +660,9 @@ describe('#onRollupWarn and #suppressUnusedImport helper functions', () => {

const config = await getViteConfig({
cwd: mockTestCwd,
getEnvironmentVariables,
mode: 'production' as const,
reactCompiler: undefined,
getEnvironmentVariables,
})

const onwarn = config.build?.rollupOptions?.onwarn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {createMockHttpServer, createMockWatcher} from '@sanity/cli-test'
import {SchemaValidationProblemGroup} from 'sanity'
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'

import {sanitySchemaExtractionPlugin} from '../plugin-schema-extraction.js'
import {SchemaExtractionError} from '../../utils/SchemaExtractionError.js'
import {sanitySchemaExtractionPlugin} from '../plugin-schema-extraction.js'

const mockRunSchemaExtraction = vi.hoisted(() => vi.fn())

Expand Down
2 changes: 1 addition & 1 deletion packages/@sanity/cli-test/src/test/createMockWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {vi, type Mock} from 'vitest'
import {type Mock, vi} from 'vitest'

/**
* @internal
Expand Down
1 change: 1 addition & 0 deletions packages/@sanity/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"react-is": "^19.2.4",
"rxjs": "catalog:",
"semver": "^7.7.4",
"skills": "^1.5.7",
"smol-toml": "^1.6.1",
"tar": "^7.5.13",
"tar-fs": "^3.1.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ function defaultFlags(
'from-create': false,
mcp: true,
'no-git': false,
skills: true,
...overrides,
} as Parameters<typeof flagsToInitOptions>[0]
}

/** Shorthand that fills in the trailing `args` and `mcpMode` parameters. */
/** Shorthand that fills in the trailing `args`, `mcpMode`, and `skillsMode`. */
function toOptions(
flags: Parameters<typeof flagsToInitOptions>[0],
isUnattended: boolean,
): ReturnType<typeof flagsToInitOptions> {
return flagsToInitOptions(flags, isUnattended, undefined, 'prompt')
return flagsToInitOptions(flags, isUnattended, undefined, 'prompt', 'auto')
}

describe('flagsToInitOptions', () => {
Expand Down Expand Up @@ -136,13 +137,24 @@ describe('flagsToInitOptions', () => {
})

test('passes mcpMode through to options', () => {
const prompt = flagsToInitOptions(defaultFlags(), false, undefined, 'prompt')
const prompt = flagsToInitOptions(defaultFlags(), false, undefined, 'prompt', 'auto')
expect(prompt.mcpMode).toBe('prompt')

const auto = flagsToInitOptions(defaultFlags(), false, undefined, 'auto')
const auto = flagsToInitOptions(defaultFlags(), false, undefined, 'auto', 'auto')
expect(auto.mcpMode).toBe('auto')

const skip = flagsToInitOptions(defaultFlags(), false, undefined, 'skip')
const skip = flagsToInitOptions(defaultFlags(), false, undefined, 'skip', 'auto')
expect(skip.mcpMode).toBe('skip')
})

test('passes skillsMode through to options', () => {
const auto = flagsToInitOptions(defaultFlags(), false, undefined, 'prompt', 'auto')
expect(auto.skillsMode).toBe('auto')

const skip = flagsToInitOptions(defaultFlags(), false, undefined, 'prompt', 'skip')
expect(skip.skillsMode).toBe('skip')

const prompt = flagsToInitOptions(defaultFlags(), false, undefined, 'prompt', 'prompt')
expect(prompt.skillsMode).toBe('prompt')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
datasetDefault: false,
fromCreate: false,
mcpMode: 'skip',
skillsMode: 'skip',
unattended: false,
}

Expand Down Expand Up @@ -112,8 +113,8 @@
describe('initAction (direct)', () => {
afterEach(() => {
vi.clearAllMocks()
const pending = nock.pendingMocks()

Check warning on line 116 in packages/@sanity/cli/src/actions/init/__tests__/initAction.test.ts

View workflow job for this annotation

GitHub Actions / lint

Caution: `nock` also has a named export `pendingMocks`. Check if you meant to write `import {pendingMocks} from 'nock'` instead
nock.cleanAll()

Check warning on line 117 in packages/@sanity/cli/src/actions/init/__tests__/initAction.test.ts

View workflow job for this annotation

GitHub Actions / lint

Caution: `nock` also has a named export `cleanAll`. Check if you meant to write `import {cleanAll} from 'nock'` instead
expect(pending, 'pending mocks').toEqual([])
})

Expand Down
38 changes: 37 additions & 1 deletion packages/@sanity/cli/src/actions/init/initAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {getProjectDefaults} from '../../util/getProjectDefaults.js'
import {validateSession} from '../auth/ensureAuthenticated.js'
import {getProviderName} from '../auth/getProviderName.js'
import {login} from '../auth/login/login.js'
import {detectAvailableEditors} from '../mcp/detectAvailableEditors.js'
import {setupMCP} from '../mcp/setupMCP.js'
import {setupSkills} from '../skills/setupSkills.js'
import {checkNextJsReactCompatibility} from './checkNextJsReactCompatibility.js'
import {determineAppTemplate} from './determineAppTemplate.js'
import {createOrAppendEnvVars} from './env/createOrAppendEnvVars.js'
Expand Down Expand Up @@ -187,7 +189,18 @@ export async function initAction(options: InitOptions, context: InitContext): Pr
workDir,
})

const mcpResult = await setupMCP({mode: options.mcpMode})
// Detect editors once, then share the result with MCP and skills setup so
// we don't pay the detection cost (filesystem probes + CLI execa calls) twice.
const detectedEditors =
options.mcpMode === 'skip' && options.skillsMode === 'skip'
? []
: await detectAvailableEditors()

const mcpResult = await setupMCP({
editors: detectedEditors,
mode: options.mcpMode,
skillsMode: options.skillsMode,
})

trace.log({
configuredEditors: mcpResult.configuredEditors,
Expand All @@ -200,6 +213,26 @@ export async function initAction(options: InitOptions, context: InitContext): Pr
}
const mcpConfigured = mcpResult.configuredEditors

async function installSkills(): Promise<void> {
if (mcpResult.skillsToInstall.length === 0) return
try {
const skillsResult = await setupSkills({agents: mcpResult.skillsToInstall})
trace.log({
installedAgents: skillsResult.installedAgents,
skipped: skillsResult.skipped,
step: 'skillsSetup',
})
if (skillsResult.error) {
trace.error(skillsResult.error)
}
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
debug('Unexpected error from setupSkills %O', err)
output.warn(`Could not install Sanity agent skills: ${err.message}`)
trace.error(err)
}
}

const {alreadyConfiguredEditors} = mcpResult
if (alreadyConfiguredEditors.length > 0) {
const label =
Expand Down Expand Up @@ -229,6 +262,7 @@ export async function initAction(options: InitOptions, context: InitContext): Pr
trace,
workDir,
})
await installSkills()
Comment thread
cursor[bot] marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing installSkills() call in --env early-return path

Medium Severity

The options.env early-return path exits without calling installSkills(), even though setupMCP has already run and may have populated mcpResult.skillsToInstall. The other two exit paths (initNextJs at line 265 and initStudio/initApp at line 310) both call installSkills(). Since skills install is global (not project-scoped), it doesn't depend on the project directory and the omission appears unintentional.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 61275bf. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 - we do setup the mcp but return early and dont try to install the skills. But it seems like it was intentionally left out @jwoods02, imo I don't see an issue doing the skills install here as well since we're automatically setting up the mcp already 🤷‍♂️

trace.complete()
return
}
Expand Down Expand Up @@ -273,6 +307,8 @@ export async function initAction(options: InitOptions, context: InitContext): Pr
projectId,
}))

await installSkills()

trace.complete()
}

Expand Down
Loading
Loading