11import type { OptimizeModel } from '../agent/index.ts'
22import type { FeaturesConfig } from '../core/config.ts'
3+ import type { LlmConfig } from './sync-shared.ts'
34import { existsSync , mkdirSync , readdirSync , readFileSync , rmSync , writeFileSync } from 'node:fs'
45import * as p from '@clack/prompts'
56import { defineCommand } from 'citty'
@@ -165,6 +166,8 @@ function resolveLocalDocs(
165166) : { docsType : 'docs' | 'llms.txt' | 'readme' , docSource : string } {
166167 const cachedDocs : Array < { path : string , content : string } > = [ ]
167168
169+ const cacheChangelog = ( ) => cacheLocalChangelog ( packageDir , packageName , version , monorepoRoot )
170+
168171 // 1. Package-level docs/
169172 const docsDir = join ( packageDir , 'docs' )
170173 if ( existsSync ( docsDir ) ) {
@@ -173,7 +176,7 @@ function resolveLocalDocs(
173176 for ( const f of mdFiles )
174177 cachedDocs . push ( { path : `docs/${ f . path } ` , content : f . content } )
175178 writeToCache ( packageName , version , cachedDocs )
176- cacheLocalChangelog ( packageDir , packageName , version )
179+ cacheChangelog ( )
177180 return { docsType : 'docs' , docSource : `local docs/ (${ mdFiles . length } files)` }
178181 }
179182 }
@@ -188,43 +191,50 @@ function resolveLocalDocs(
188191 for ( const f of mdFiles )
189192 cachedDocs . push ( { path : `docs/${ f . path } ` , content : f . content } )
190193 writeToCache ( packageName , version , cachedDocs )
191- cacheLocalChangelog ( packageDir , packageName , version )
194+ cacheChangelog ( )
192195 return { docsType : 'docs' , docSource : `monorepo ${ candidate } / (${ mdFiles . length } files)` }
193196 }
194197 }
195198 }
196199 }
197200
198- // 3. llms.txt
199- const llmsPath = join ( packageDir , 'llms.txt' )
200- if ( existsSync ( llmsPath ) ) {
201- const content = readFileSync ( llmsPath , 'utf-8' )
202- cachedDocs . push ( { path : 'llms.txt' , content } )
203- writeToCache ( packageName , version , cachedDocs )
204- cacheLocalChangelog ( packageDir , packageName , version )
205- return { docsType : 'llms.txt' , docSource : 'local llms.txt' }
201+ // 3. llms.txt (package dir, then monorepo root)
202+ for ( const dir of [ packageDir , monorepoRoot ] . filter ( Boolean ) as string [ ] ) {
203+ const llmsPath = join ( dir , 'llms.txt' )
204+ if ( existsSync ( llmsPath ) ) {
205+ cachedDocs . push ( { path : 'llms.txt' , content : readFileSync ( llmsPath , 'utf-8' ) } )
206+ writeToCache ( packageName , version , cachedDocs )
207+ cacheChangelog ( )
208+ const source = dir === packageDir ? 'local llms.txt' : 'monorepo llms.txt'
209+ return { docsType : 'llms.txt' , docSource : source }
210+ }
206211 }
207212
208- // 4. README.md
209- const readmeFile = readdirSync ( packageDir ) . find ( f => / ^ r e a d m e \. m d $ / i. test ( f ) )
210- if ( readmeFile ) {
211- const content = readFileSync ( join ( packageDir , readmeFile ) , 'utf-8' )
212- cachedDocs . push ( { path : 'docs/README.md' , content } )
213- writeToCache ( packageName , version , cachedDocs )
214- cacheLocalChangelog ( packageDir , packageName , version )
215- return { docsType : 'readme' , docSource : 'local README.md' }
213+ // 4. README.md (package dir, then monorepo root)
214+ for ( const dir of [ packageDir , monorepoRoot ] . filter ( Boolean ) as string [ ] ) {
215+ const readmeFile = readdirSync ( dir ) . find ( f => / ^ r e a d m e \. m d $ / i. test ( f ) )
216+ if ( readmeFile ) {
217+ cachedDocs . push ( { path : 'docs/README.md' , content : readFileSync ( join ( dir , readmeFile ) , 'utf-8' ) } )
218+ writeToCache ( packageName , version , cachedDocs )
219+ cacheChangelog ( )
220+ const source = dir === packageDir ? 'local README.md' : 'monorepo README.md'
221+ return { docsType : 'readme' , docSource : source }
222+ }
216223 }
217224
218- cacheLocalChangelog ( packageDir , packageName , version )
225+ cacheChangelog ( )
219226 return { docsType : 'readme' , docSource : 'none' }
220227}
221228
222- function cacheLocalChangelog ( dir : string , packageName : string , version : string ) : void {
223- const changelogFile = [ 'CHANGELOG.md' , 'changelog.md' ] . find ( f => existsSync ( join ( dir , f ) ) )
224- if ( changelogFile ) {
229+ function cacheLocalChangelog ( dir : string , packageName : string , version : string , monorepoRoot ?: string ) : void {
230+ const candidates = [ 'CHANGELOG.md' , 'changelog.md' ]
231+ const changelogFile = candidates . find ( f => existsSync ( join ( dir , f ) ) )
232+ || ( monorepoRoot ? candidates . find ( f => existsSync ( join ( monorepoRoot , f ) ) ) : undefined )
233+ const changelogDir = changelogFile && existsSync ( join ( dir , changelogFile ) ) ? dir : monorepoRoot
234+ if ( changelogFile && changelogDir ) {
225235 writeToCache ( packageName , version , [ {
226236 path : `releases/${ changelogFile } ` ,
227- content : readFileSync ( join ( dir , changelogFile ) , 'utf-8' ) ,
237+ content : readFileSync ( join ( changelogDir , changelogFile ) , 'utf-8' ) ,
228238 } ] )
229239 }
230240}
@@ -307,18 +317,33 @@ function patchPackageJsonFiles(packageDir: string): void {
307317 const pkg = JSON . parse ( raw )
308318
309319 if ( ! Array . isArray ( pkg . files ) ) {
310- // Create files array with common defaults
311- pkg . files = [ 'dist' , 'skills' ]
312- writeFileSync ( pkgPath , `${ JSON . stringify ( pkg , null , 2 ) } \n` )
313- p . log . success ( 'Created `files` array in package.json with `["dist", "skills"]`. Verify this matches your package.' )
320+ p . log . warn ( 'No `files` array in package.json. Add `"skills"` to your files array manually.' )
314321 return
315322 }
316323
317324 if ( pkg . files . some ( ( f : string ) => f === 'skills' || f === 'skills/' || f === 'skills/**' ) )
318325 return
319326
320- pkg . files . push ( 'skills' )
321- writeFileSync ( pkgPath , `${ JSON . stringify ( pkg , null , 2 ) } \n` )
327+ // Targeted insertion: find the closing bracket of the files array and insert before it
328+ // This preserves the original formatting (indentation, trailing newlines, etc.)
329+ const filesMatch = raw . match ( / " f i l e s " \s * : \s * \[ ( [ ^ \] ] * ) \] / )
330+ if ( ! filesMatch ) {
331+ // Fallback: full rewrite
332+ pkg . files . push ( 'skills' )
333+ writeFileSync ( pkgPath , `${ JSON . stringify ( pkg , null , 2 ) } \n` )
334+ p . log . success ( 'Added `"skills"` to package.json files array' )
335+ return
336+ }
337+
338+ const inner = filesMatch [ 1 ]
339+ const trimmed = inner . trimEnd ( )
340+ // Detect indentation from existing entries
341+ const entryMatch = inner . match ( / \n ( \s + ) " / )
342+ const indent = entryMatch ? entryMatch [ 1 ] : ' '
343+ const needsComma = trimmed . length > 0 && ! trimmed . endsWith ( ',' )
344+ const insertion = `${ needsComma ? ',' : '' } \n${ indent } "skills"`
345+ const patched = raw . replace ( filesMatch [ 0 ] , `"files": [${ trimmed } ${ insertion } \n${ indent . slice ( 2 ) || ' ' } ]` )
346+ writeFileSync ( pkgPath , patched )
322347 p . log . success ( 'Added `"skills"` to package.json files array' )
323348}
324349
@@ -332,8 +357,7 @@ async function authorSinglePackage(opts: {
332357 repoUrl ?: string
333358 monorepoRoot ?: string
334359 out ?: string
335- model ?: OptimizeModel
336- yes ?: boolean
360+ llmConfig ?: LlmConfig | null
337361 force ?: boolean
338362 debug ?: boolean
339363} ) : Promise < string | null > {
@@ -405,52 +429,49 @@ async function authorSinglePackage(opts: {
405429 writeFileSync ( join ( outDir , 'SKILL.md' ) , baseSkillMd )
406430 p . log . success ( `Created base skill: ${ relative ( packageDir , outDir ) } ` )
407431
408- // LLM enhancement
409- const globalConfig = readConfig ( )
410- if ( ! globalConfig . skipLlm && ( ! opts . yes || opts . model ) ) {
411- const llmConfig = await selectLlmConfig ( opts . model )
412- if ( llmConfig ?. promptOnly ) {
413- writePromptFiles ( {
414- packageName,
415- skillDir : outDir ,
416- version,
417- hasIssues,
418- hasDiscussions,
419- hasReleases,
420- hasChangelog,
421- docsType,
422- hasShippedDocs : false ,
423- pkgFiles : [ ] ,
424- sections : llmConfig . sections ,
425- customPrompt : llmConfig . customPrompt ,
426- features,
427- } )
428- }
429- else if ( llmConfig ) {
430- p . log . step ( getModelLabel ( llmConfig . model ) )
431- await enhanceSkillWithLLM ( {
432- packageName,
433- version,
434- skillDir : outDir ,
435- dirName : sanitizedName ,
436- model : llmConfig . model ,
437- resolved : { repoUrl : opts . repoUrl } ,
438- relatedSkills : [ ] ,
439- hasIssues,
440- hasDiscussions,
441- hasReleases,
442- hasChangelog,
443- docsType,
444- hasShippedDocs : false ,
445- pkgFiles : [ ] ,
446- force : opts . force ,
447- debug : opts . debug ,
448- sections : llmConfig . sections ,
449- customPrompt : llmConfig . customPrompt ,
450- features,
451- eject : true ,
452- } )
453- }
432+ // LLM enhancement (config resolved by caller)
433+ const llmConfig = opts . llmConfig
434+ if ( llmConfig ?. promptOnly ) {
435+ writePromptFiles ( {
436+ packageName,
437+ skillDir : outDir ,
438+ version,
439+ hasIssues,
440+ hasDiscussions,
441+ hasReleases,
442+ hasChangelog,
443+ docsType,
444+ hasShippedDocs : false ,
445+ pkgFiles : [ ] ,
446+ sections : llmConfig . sections ,
447+ customPrompt : llmConfig . customPrompt ,
448+ features,
449+ } )
450+ }
451+ else if ( llmConfig ) {
452+ p . log . step ( getModelLabel ( llmConfig . model ) )
453+ await enhanceSkillWithLLM ( {
454+ packageName,
455+ version,
456+ skillDir : outDir ,
457+ dirName : sanitizedName ,
458+ model : llmConfig . model ,
459+ resolved : { repoUrl : opts . repoUrl } ,
460+ relatedSkills : [ ] ,
461+ hasIssues,
462+ hasDiscussions,
463+ hasReleases,
464+ hasChangelog,
465+ docsType,
466+ hasShippedDocs : false ,
467+ pkgFiles : [ ] ,
468+ force : opts . force ,
469+ debug : opts . debug ,
470+ sections : llmConfig . sections ,
471+ customPrompt : llmConfig . customPrompt ,
472+ features,
473+ eject : true ,
474+ } )
454475 }
455476
456477 // Clean up .skilld/ symlinks → eject references as real files
@@ -468,6 +489,13 @@ async function authorSinglePackage(opts: {
468489
469490// ── Main command ──
470491
492+ async function resolveLlmConfig ( model ?: OptimizeModel , yes ?: boolean ) : Promise < LlmConfig | null | undefined > {
493+ const globalConfig = readConfig ( )
494+ if ( globalConfig . skipLlm || ( yes && ! model ) )
495+ return undefined
496+ return selectLlmConfig ( model )
497+ }
498+
471499async function authorCommand ( opts : {
472500 out ?: string
473501 model ?: OptimizeModel
@@ -483,6 +511,11 @@ async function authorCommand(opts: {
483511 if ( monoPackages && monoPackages . length > 0 ) {
484512 p . intro ( `\x1B[1m\x1B[35mskilld\x1B[0m author \x1B[90m(monorepo: ${ monoPackages . length } packages)\x1B[0m` )
485513
514+ if ( opts . out ) {
515+ p . log . error ( '--out is not supported in monorepo mode (each package gets its own skills/ directory)' )
516+ return
517+ }
518+
486519 const selected = guard ( await p . multiselect ( {
487520 message : 'Which packages should ship skills?' ,
488521 options : monoPackages . map ( pkg => ( {
@@ -495,6 +528,16 @@ async function authorCommand(opts: {
495528 if ( selected . length === 0 )
496529 return
497530
531+ // Resolve LLM config once for all packages
532+ const llmConfig = await resolveLlmConfig ( opts . model , opts . yes )
533+
534+ // Resolve monorepo-level repoUrl for packages that lack their own
535+ const rootPkgPath = join ( cwd , 'package.json' )
536+ const rootPkg = JSON . parse ( readFileSync ( rootPkgPath , 'utf-8' ) )
537+ const rootRepoUrl = typeof rootPkg . repository === 'string'
538+ ? rootPkg . repository
539+ : rootPkg . repository ?. url ?. replace ( / ^ g i t \+ / , '' ) . replace ( / \. g i t $ / , '' )
540+
498541 const results : Array < { name : string , outDir : string } > = [ ]
499542
500543 for ( const pkg of selected ) {
@@ -504,11 +547,9 @@ async function authorCommand(opts: {
504547 packageName : pkg . name ,
505548 version : pkg . version ,
506549 description : pkg . description ,
507- repoUrl : pkg . repoUrl ,
550+ repoUrl : pkg . repoUrl || rootRepoUrl ,
508551 monorepoRoot : cwd ,
509- out : opts . out ,
510- model : opts . model ,
511- yes : opts . yes ,
552+ llmConfig,
512553 force : opts . force ,
513554 debug : opts . debug ,
514555 } )
@@ -539,15 +580,16 @@ async function authorCommand(opts: {
539580
540581 p . intro ( `\x1B[1m\x1B[35mskilld\x1B[0m author \x1B[36m${ packageName } \x1B[0m@${ version } ` )
541582
583+ const llmConfig = await resolveLlmConfig ( opts . model , opts . yes )
584+
542585 const outDir = await authorSinglePackage ( {
543586 packageDir : cwd ,
544587 packageName,
545588 version,
546589 description : pkgInfo . description ,
547590 repoUrl,
548591 out : opts . out ,
549- model : opts . model ,
550- yes : opts . yes ,
592+ llmConfig,
551593 force : opts . force ,
552594 debug : opts . debug ,
553595 } )
0 commit comments