Skip to content

Commit 5536cb9

Browse files
committed
fix(cli): accept source prefixes in the wizard package prompt
The wizard passed manually entered packages through unparsed, so the `npm:` prefix documented for `skilld add` reached `getCacheDir` as a literal package name and aborted the run. Normalise with `parseSkillInput` as `add` does, and direct non-npm sources to `skilld add`. `resolvePkgDir` treats an unusable name as a cache miss rather than propagating the validation error, and rejects an empty name instead of resolving to `node_modules`.
1 parent 5687ca7 commit 5536cb9

4 files changed

Lines changed: 48 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ skilld
146146
# Add skills for specific package(s) — npm: prefix for registry packages
147147
skilld add npm:vue npm:nuxt npm:pinia
148148

149+
# The same prefixes work in the interactive wizard's package prompt
150+
149151
# Add a pre-authored skill from a GitHub repo
150152
skilld add gh:vercel-labs/agent-skills
151153

src/cli.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { runWizard } from './commands/wizard.ts'
2121
import { timedSpinner } from './core/formatting.ts'
2222
import { getProjectState, hasCompletedWizard, isOutdated, readConfig, semverGt } from './core/index.ts'
2323
import { readPackageJsonSafe } from './core/package-json.ts'
24+
import { parseSkillInput } from './core/prefix.ts'
2425
import { COMMA_OR_WHITESPACE_RE, VERSION_RANGE_PREFIX_RE } from './core/regex.ts'
2526
import { iterateSkills } from './core/skills.ts'
2627
import { fetchLatestVersion, fetchNpmRegistryMeta } from './sources/index.ts'
@@ -59,6 +60,19 @@ function deprecatedForwarder(
5960

6061
// ── Subcommands (lazy-loaded) ──
6162

63+
function toPackageNames(tokens: string[]): string[] | null {
64+
const names: string[] = []
65+
for (const token of tokens) {
66+
const source = parseSkillInput(token)
67+
if (source.type !== 'npm' && source.type !== 'bare') {
68+
p.log.error(`${token} is not an npm package. Install it with \`skilld add ${token}\`.`)
69+
return null
70+
}
71+
names.push(source.package)
72+
}
73+
return names
74+
}
75+
6276
const SUBCOMMAND_NAMES = ['add', 'eject', 'update', 'info', 'list', 'config', 'remove', 'install', 'uninstall', 'search', 'cache', 'validate', 'assemble', 'setup', 'prepare', 'author', 'publish', 'upload', 'login', 'logout', 'whoami', 'pull']
6377

6478
// ── Main command ──
@@ -277,7 +291,7 @@ const main = defineCommand({
277291
if (source === 'manual') {
278292
const input = await p.text({
279293
message: 'Enter package names (space or comma-separated)',
280-
placeholder: 'vue nuxt pinia',
294+
placeholder: 'vue npm:nuxt pinia',
281295
})
282296
if (p.isCancel(input)) {
283297
if (!hasPkgJson) {
@@ -290,7 +304,10 @@ const main = defineCommand({
290304
p.log.warn('No packages entered')
291305
continue
292306
}
293-
selected = input.split(COMMA_OR_WHITESPACE_RE).map(s => s.trim()).filter(Boolean)
307+
const names = toPackageNames(input.split(COMMA_OR_WHITESPACE_RE).map(s => s.trim()).filter(Boolean))
308+
if (!names)
309+
continue
310+
selected = names
294311
if (selected.length === 0) {
295312
p.log.warn('No valid packages entered')
296313
continue
@@ -534,11 +551,14 @@ const main = defineCommand({
534551
if (source === 'manual') {
535552
const input = guard(await p.text({
536553
message: 'Enter package names (space or comma-separated)',
537-
placeholder: 'vue nuxt pinia',
554+
placeholder: 'vue npm:nuxt pinia',
538555
}))
539556
if (!input)
540557
return
541-
selected = input.split(COMMA_OR_WHITESPACE_RE).map(s => s.trim()).filter(Boolean)
558+
const names = toPackageNames(input.split(COMMA_OR_WHITESPACE_RE).map(s => s.trim()).filter(Boolean))
559+
if (!names)
560+
return
561+
selected = names
542562
if (selected.length === 0)
543563
return
544564
}

src/core/prepare.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,21 @@ function toStorageName(name: string): string {
2424

2525
/** Resolve package directory: node_modules first, then global cache */
2626
export function resolvePkgDir(name: string, cwd: string, version?: string): string | null {
27+
if (!name)
28+
return null
29+
2730
const nodeModulesPath = join(cwd, 'node_modules', name)
2831
if (existsSync(nodeModulesPath))
2932
return nodeModulesPath
3033

3134
if (version) {
32-
const cachedPkgDir = join(getCacheDir(name, version), 'pkg')
35+
let cachedPkgDir: string
36+
try {
37+
cachedPkgDir = join(getCacheDir(name, version), 'pkg')
38+
}
39+
catch {
40+
return null
41+
}
3342
if (existsSync(join(cachedPkgDir, 'package.json')))
3443
return cachedPkgDir
3544
}

test/unit/pkg-dir-probe.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { getShippedSkills, resolvePkgDir } from '../../src/core/prepare.ts'
3+
4+
describe('package dir probing', () => {
5+
it.each(['npm:vue', 'gh:owner/repo', '../escape', ''])('returns null for %j', (name) => {
6+
expect(resolvePkgDir(name, process.cwd(), '1.0.0')).toBeNull()
7+
})
8+
9+
it.each(['npm:vue', '../escape'])('reports no shipped skills for %j', (name) => {
10+
expect(getShippedSkills(name, process.cwd(), '1.0.0')).toEqual([])
11+
})
12+
})

0 commit comments

Comments
 (0)