Skip to content

Commit 8fd064b

Browse files
committed
fix: improve author command robustness
- Prompt LLM config once for all packages in monorepo mode (not per-package) - Error on --out in monorepo mode (each package needs its own skills/ dir) - Fall back to monorepo root for CHANGELOG, llms.txt, and README resolution - Inherit repoUrl from monorepo root when child packages lack their own - Preserve package.json formatting when patching files array (targeted insertion)
1 parent f9fecf9 commit 8fd064b

1 file changed

Lines changed: 125 additions & 83 deletions

File tree

src/commands/author.ts

Lines changed: 125 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { OptimizeModel } from '../agent/index.ts'
22
import type { FeaturesConfig } from '../core/config.ts'
3+
import type { LlmConfig } from './sync-shared.ts'
34
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
45
import * as p from '@clack/prompts'
56
import { 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 => /^readme\.md$/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 => /^readme\.md$/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(/"files"\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+
471499
async 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(/^git\+/, '').replace(/\.git$/, '')
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

Comments
 (0)