@@ -20,6 +20,10 @@ import { homedir } from 'node:os';
2020
2121const mockedGetResourceRoot = vi . mocked ( getResourceRoot ) ;
2222const mockedHomedir = vi . mocked ( homedir ) ;
23+ const agentsGuidanceLine =
24+ '- If using XcodeBuildMCP, use the installed XcodeBuildMCP skill before calling XcodeBuildMCP tools.' ;
25+ const legacyAgentsGuidanceLine =
26+ '- If using XcodeBuildMCP, first find and read the installed XcodeBuildMCP skill before calling XcodeBuildMCP tools.' ;
2327
2428function loadInitModule ( ) {
2529 return import ( '../init.ts' ) ;
@@ -126,6 +130,58 @@ describe('init command', () => {
126130
127131 stdoutSpy . mockRestore ( ) ;
128132 } ) ;
133+
134+ it ( 'skips Claude for MCP skill in auto-detect mode' , async ( ) => {
135+ const fakeHome = join ( tempDir , 'home-auto-skip-claude' ) ;
136+ mkdirSync ( join ( fakeHome , '.claude' ) , { recursive : true } ) ;
137+ mkdirSync ( join ( fakeHome , '.cursor' ) , { recursive : true } ) ;
138+ mockedHomedir . mockReturnValue ( fakeHome ) ;
139+
140+ const yargs = ( await import ( 'yargs' ) ) . default ;
141+ const mod = await loadInitModule ( ) ;
142+
143+ const app = yargs ( [ 'init' , '--skill' , 'mcp' ] ) . scriptName ( '' ) ;
144+ mod . registerInitCommand ( app ) ;
145+
146+ const stdoutSpy = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true ) ;
147+ await app . parseAsync ( ) ;
148+
149+ expect ( existsSync ( join ( fakeHome , '.claude' , 'skills' , 'xcodebuildmcp' , 'SKILL.md' ) ) ) . toBe (
150+ false ,
151+ ) ;
152+ expect ( existsSync ( join ( fakeHome , '.cursor' , 'skills' , 'xcodebuildmcp' , 'SKILL.md' ) ) ) . toBe (
153+ true ,
154+ ) ;
155+
156+ const output = stdoutSpy . mock . calls . map ( ( c ) => String ( c [ 0 ] ) ) . join ( '' ) ;
157+ expect ( output ) . toContain ( 'Skipped Claude Code' ) ;
158+
159+ stdoutSpy . mockRestore ( ) ;
160+ } ) ;
161+
162+ it ( 'allows explicit Claude MCP install with --client claude' , async ( ) => {
163+ const fakeHome = join ( tempDir , 'home-explicit-claude' ) ;
164+ mkdirSync ( join ( fakeHome , '.claude' ) , { recursive : true } ) ;
165+ mockedHomedir . mockReturnValue ( fakeHome ) ;
166+
167+ const yargs = ( await import ( 'yargs' ) ) . default ;
168+ const mod = await loadInitModule ( ) ;
169+
170+ const app = yargs ( [ 'init' , '--client' , 'claude' , '--skill' , 'mcp' ] ) . scriptName ( '' ) ;
171+ mod . registerInitCommand ( app ) ;
172+
173+ const stdoutSpy = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true ) ;
174+ await app . parseAsync ( ) ;
175+
176+ expect ( existsSync ( join ( fakeHome , '.claude' , 'skills' , 'xcodebuildmcp' , 'SKILL.md' ) ) ) . toBe (
177+ true ,
178+ ) ;
179+
180+ const output = stdoutSpy . mock . calls . map ( ( c ) => String ( c [ 0 ] ) ) . join ( '' ) ;
181+ expect ( output ) . not . toContain ( 'Skipped Claude Code' ) ;
182+
183+ stdoutSpy . mockRestore ( ) ;
184+ } ) ;
129185 } ) ;
130186
131187 describe ( 'conflict handling' , ( ) => {
@@ -320,6 +376,127 @@ describe('init command', () => {
320376 } ) ;
321377 } ) ;
322378
379+ describe ( 'AGENTS.md guidance on skill install' , ( ) => {
380+ it ( 'creates project-level AGENTS.md when missing' , async ( ) => {
381+ const dest = join ( tempDir , 'skills' ) ;
382+ const projectRoot = join ( tempDir , 'project-create' ) ;
383+ mkdirSync ( dest , { recursive : true } ) ;
384+ mkdirSync ( projectRoot , { recursive : true } ) ;
385+
386+ const yargs = ( await import ( 'yargs' ) ) . default ;
387+ const mod = await loadInitModule ( ) ;
388+
389+ const app = yargs ( [ 'init' , '--dest' , dest , '--skill' , 'cli' ] ) . scriptName ( '' ) ;
390+ mod . registerInitCommand ( app , { workspaceRoot : projectRoot } ) ;
391+
392+ const stdoutSpy = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true ) ;
393+ await app . parseAsync ( ) ;
394+
395+ const agentsPath = join ( projectRoot , 'AGENTS.md' ) ;
396+ expect ( existsSync ( agentsPath ) ) . toBe ( true ) ;
397+ expect ( readFileSync ( agentsPath , 'utf8' ) ) . toContain ( agentsGuidanceLine ) ;
398+
399+ const output = stdoutSpy . mock . calls . map ( ( c ) => String ( c [ 0 ] ) ) . join ( '' ) ;
400+ expect ( output ) . toContain ( 'Created AGENTS.md with XcodeBuildMCP guidance' ) ;
401+
402+ stdoutSpy . mockRestore ( ) ;
403+ } ) ;
404+
405+ it ( 'shows diff and errors in non-interactive mode when AGENTS.md exists and --force is not set' , async ( ) => {
406+ const dest = join ( tempDir , 'skills' ) ;
407+ const projectRoot = join ( tempDir , 'project-non-interactive' ) ;
408+ mkdirSync ( dest , { recursive : true } ) ;
409+ mkdirSync ( projectRoot , { recursive : true } ) ;
410+ writeFileSync ( join ( projectRoot , 'AGENTS.md' ) , '# Existing\n' , 'utf8' ) ;
411+
412+ const originalIsTTY = process . stdin . isTTY ;
413+ Object . defineProperty ( process . stdin , 'isTTY' , { value : false , configurable : true } ) ;
414+
415+ const yargs = ( await import ( 'yargs' ) ) . default ;
416+ const mod = await loadInitModule ( ) ;
417+
418+ const app = yargs ( [ 'init' , '--dest' , dest , '--skill' , 'cli' ] ) . scriptName ( '' ) . fail ( false ) ;
419+ mod . registerInitCommand ( app , { workspaceRoot : projectRoot } ) ;
420+
421+ const stdoutSpy = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true ) ;
422+ await expect ( app . parseAsync ( ) ) . rejects . toThrow (
423+ 'AGENTS.md exists and requires confirmation to update' ,
424+ ) ;
425+
426+ const output = stdoutSpy . mock . calls . map ( ( c ) => String ( c [ 0 ] ) ) . join ( '' ) ;
427+ expect ( output ) . toContain ( 'Proposed update for' ) ;
428+ expect ( output ) . toContain ( '--- AGENTS.md' ) ;
429+ expect ( output ) . toContain ( '+++ AGENTS.md' ) ;
430+ expect ( output ) . toContain ( agentsGuidanceLine ) ;
431+
432+ stdoutSpy . mockRestore ( ) ;
433+ Object . defineProperty ( process . stdin , 'isTTY' , { value : originalIsTTY , configurable : true } ) ;
434+ } ) ;
435+
436+ it ( 'updates existing AGENTS.md with --force without prompting' , async ( ) => {
437+ const dest = join ( tempDir , 'skills' ) ;
438+ const projectRoot = join ( tempDir , 'project-force' ) ;
439+ mkdirSync ( dest , { recursive : true } ) ;
440+ mkdirSync ( projectRoot , { recursive : true } ) ;
441+ writeFileSync ( join ( projectRoot , 'AGENTS.md' ) , '# Existing\n' , 'utf8' ) ;
442+
443+ const originalIsTTY = process . stdin . isTTY ;
444+ Object . defineProperty ( process . stdin , 'isTTY' , { value : false , configurable : true } ) ;
445+
446+ const yargs = ( await import ( 'yargs' ) ) . default ;
447+ const mod = await loadInitModule ( ) ;
448+
449+ const app = yargs ( [ 'init' , '--dest' , dest , '--skill' , 'cli' , '--force' ] ) . scriptName ( '' ) ;
450+ mod . registerInitCommand ( app , { workspaceRoot : projectRoot } ) ;
451+
452+ const stdoutSpy = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true ) ;
453+ await app . parseAsync ( ) ;
454+
455+ const agentsContent = readFileSync ( join ( projectRoot , 'AGENTS.md' ) , 'utf8' ) ;
456+ expect ( agentsContent ) . toContain ( '# Existing' ) ;
457+ expect ( agentsContent ) . toContain ( agentsGuidanceLine ) ;
458+
459+ const output = stdoutSpy . mock . calls . map ( ( c ) => String ( c [ 0 ] ) ) . join ( '' ) ;
460+ expect ( output ) . toContain ( 'Proposed update for' ) ;
461+ expect ( output ) . toContain ( 'Updated AGENTS.md at' ) ;
462+
463+ stdoutSpy . mockRestore ( ) ;
464+ Object . defineProperty ( process . stdin , 'isTTY' , { value : originalIsTTY , configurable : true } ) ;
465+ } ) ;
466+
467+ it ( 'replaces legacy XcodeBuildMCP guidance line without appending duplicate' , async ( ) => {
468+ const dest = join ( tempDir , 'skills' ) ;
469+ const projectRoot = join ( tempDir , 'project-legacy-guidance' ) ;
470+ mkdirSync ( dest , { recursive : true } ) ;
471+ mkdirSync ( projectRoot , { recursive : true } ) ;
472+ writeFileSync (
473+ join ( projectRoot , 'AGENTS.md' ) ,
474+ `# Existing\n\n${ legacyAgentsGuidanceLine } \n` ,
475+ 'utf8' ,
476+ ) ;
477+
478+ const yargs = ( await import ( 'yargs' ) ) . default ;
479+ const mod = await loadInitModule ( ) ;
480+
481+ const app = yargs ( [ 'init' , '--dest' , dest , '--skill' , 'cli' ] ) . scriptName ( '' ) ;
482+ mod . registerInitCommand ( app , { workspaceRoot : projectRoot } ) ;
483+
484+ const stdoutSpy = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true ) ;
485+ await app . parseAsync ( ) ;
486+
487+ const agentsContent = readFileSync ( join ( projectRoot , 'AGENTS.md' ) , 'utf8' ) ;
488+ expect ( agentsContent ) . toContain ( agentsGuidanceLine ) ;
489+ expect ( agentsContent ) . not . toContain ( legacyAgentsGuidanceLine ) ;
490+ expect (
491+ agentsContent . match (
492+ new RegExp ( agentsGuidanceLine . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) , 'g' ) ,
493+ ) ?. length ,
494+ ) . toBe ( 1 ) ;
495+
496+ stdoutSpy . mockRestore ( ) ;
497+ } ) ;
498+ } ) ;
499+
323500 describe ( 'error cases' , ( ) => {
324501 it ( 'errors when --dest points to filesystem root' , async ( ) => {
325502 const rootDest = '/' ;
0 commit comments