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
39 changes: 38 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.
Comment thread
cursor[bot] marked this conversation as resolved.
trace.complete()
return
}
Expand All @@ -246,6 +280,7 @@ export async function initAction(options: InitOptions, context: InitContext): Pr
outputPath,
})
await writeStagingEnvIfNeeded(output, outputPath)
await installSkills()
trace.complete()
return
}
Expand Down Expand Up @@ -273,6 +308,8 @@ export async function initAction(options: InitOptions, context: InitContext): Pr
projectId,
}))

await installSkills()

trace.complete()
}

Expand Down
Loading
Loading