@skilldeck/core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli.mjs +6 -3
- package/lib/install.mjs +60 -20
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ export const COMMON_OPTIONS = {
|
|
|
7
7
|
agent: { type: 'string', short: 'a', default: 'claude' },
|
|
8
8
|
project: { type: 'boolean', short: 'p', default: false },
|
|
9
9
|
force: { type: 'boolean', short: 'f', default: false },
|
|
10
|
+
skill: { type: 'string', short: 's', multiple: true },
|
|
10
11
|
'dry-run': { type: 'boolean', default: false },
|
|
11
12
|
help: { type: 'boolean', short: 'h', default: false },
|
|
12
13
|
version: { type: 'boolean', short: 'v', default: false },
|
|
@@ -15,6 +16,7 @@ export const COMMON_OPTIONS = {
|
|
|
15
16
|
export const OPTIONS_HELP = `Options:
|
|
16
17
|
-a, --agent <name> claude (default), codex, agents, all, or a comma list
|
|
17
18
|
-p, --project install into ./.claude (or ./.codex, ./.agents) instead of your home
|
|
19
|
+
-s, --skill <name> only these skills from the pack (repeat, or comma-separate)
|
|
18
20
|
-f, --force replace skill folders that SkillDeck didn't install
|
|
19
21
|
--dry-run show what would happen without writing anything
|
|
20
22
|
-h, --help show help
|
|
@@ -35,14 +37,15 @@ export function parse(argv, extra = {}) {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export function toOpts(values) {
|
|
38
|
-
|
|
40
|
+
const skills = (values.skill ?? []).flatMap((s) => s.split(',')).map((s) => s.trim()).filter(Boolean)
|
|
41
|
+
return { agent: values.agent, project: values.project, force: values.force, dryRun: values['dry-run'], skills: skills.length ? skills : undefined }
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export function printInstall(pack, results) {
|
|
42
45
|
for (const r of results) {
|
|
43
46
|
const verb = r.dryRun ? 'would install' : r.upgraded ? 'updated' : 'installed'
|
|
44
47
|
console.log(`${green('✓')} ${bold(pack.name)}@${pack.version} ${verb} for ${r.label} ${dim('→ ' + tilde(r.skillsDir))}`)
|
|
45
|
-
console.log(dim(` skills: ${r.skills.join(', ')}`))
|
|
48
|
+
console.log(dim(` skills: ${r.skills.join(', ')}${r.partial ? ' (selected)' : ''}`))
|
|
46
49
|
if (r.hook) console.log(dim(` hook: ${r.hook.event} in ${tilde(r.hook.settingsFile)}`))
|
|
47
50
|
if (r.note) console.log(yellow(` note: ${r.note}`))
|
|
48
51
|
}
|
|
@@ -50,7 +53,7 @@ export function printInstall(pack, results) {
|
|
|
50
53
|
|
|
51
54
|
export function printUninstall(name, results) {
|
|
52
55
|
for (const r of results) {
|
|
53
|
-
if (r.removed) console.log(`${green('✓')} removed ${bold(name)} from ${r.label}${r.hook ? dim(' (and its hook)') : ''}`)
|
|
56
|
+
if (r.removed) console.log(`${green('✓')} removed ${bold(r.partial ? `${name}: ${r.skills.join(', ')}` : name)} from ${r.label}${r.hook ? dim(' (and its hook)') : ''}`)
|
|
54
57
|
else console.log(dim(`- ${name} is not installed for ${r.label}`))
|
|
55
58
|
}
|
|
56
59
|
}
|
package/lib/install.mjs
CHANGED
|
@@ -15,14 +15,37 @@ function hookCommandFor(target, pack) {
|
|
|
15
15
|
return target.hookCommand(`${pack.name}/${pack.hook.script}`)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/** Skills a hook depends on (e.g. the bootstrap skill it injects). The hook is installed only with them. */
|
|
19
|
+
const hookSkills = (pack) => (pack.hook?.include ?? []).filter((p) => p.startsWith('skills/')).map((p) => p.split('/')[1])
|
|
20
|
+
|
|
21
|
+
/** Validate an optional `skills` subset against what the pack ships. */
|
|
22
|
+
function selectSkills(pack, wanted) {
|
|
23
|
+
if (!wanted?.length) return { selected: pack.skills, partial: false }
|
|
24
|
+
const unknown = wanted.filter((s) => !pack.skills.includes(s))
|
|
25
|
+
if (unknown.length) throw new Error(`${pack.name} has no skill named ${unknown.join(', ')}. Available: ${pack.skills.join(', ')}`)
|
|
26
|
+
const selected = pack.skills.filter((s) => wanted.includes(s))
|
|
27
|
+
return { selected, partial: selected.length < pack.skills.length }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function removeHookFiles(hook) {
|
|
31
|
+
removeHook(hook.settingsFile, { event: hook.event, command: hook.command })
|
|
32
|
+
fs.rmSync(hook.dir, { recursive: true, force: true })
|
|
33
|
+
const parent = path.dirname(hook.dir)
|
|
34
|
+
if (fs.existsSync(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Install a pack (or, with `skills`, just some of its skills) for each requested agent.
|
|
39
|
+
* Throws before writing anything if a conflict is found.
|
|
40
|
+
*/
|
|
19
41
|
export function install(pack, opts = {}) {
|
|
20
42
|
const { force = false, dryRun = false } = opts
|
|
43
|
+
const { selected, partial } = selectSkills(pack, opts.skills)
|
|
21
44
|
const agents = expandAgents(opts.agent)
|
|
22
45
|
const plans = agents.map((agent) => {
|
|
23
46
|
const target = resolveTarget(agent, opts)
|
|
24
47
|
const manifest = readManifest(target.skillsDir)
|
|
25
|
-
const conflicts =
|
|
48
|
+
const conflicts = selected.filter((s) => {
|
|
26
49
|
const owner = ownerOf(manifest, s)
|
|
27
50
|
return owner !== pack.name && (owner || fs.existsSync(path.join(target.skillsDir, s)))
|
|
28
51
|
})
|
|
@@ -35,26 +58,32 @@ export function install(pack, opts = {}) {
|
|
|
35
58
|
throw new Error(`These skill folders already exist and weren't installed by ${pack.name}:\n${lines.join('\n')}\nRe-run with --force to replace them.`)
|
|
36
59
|
}
|
|
37
60
|
|
|
61
|
+
const wantsHook = Boolean(pack.hook) && (!partial || hookSkills(pack).every((s) => selected.includes(s)))
|
|
62
|
+
|
|
38
63
|
return plans.map(({ target, manifest }) => {
|
|
39
64
|
const previous = manifest.packs[pack.name]
|
|
40
|
-
const result = { agent: target.agent, label: target.label, skillsDir: target.skillsDir, skills:
|
|
65
|
+
const result = { agent: target.agent, label: target.label, skillsDir: target.skillsDir, skills: selected, partial, hook: null, upgraded: Boolean(previous), dryRun }
|
|
41
66
|
|
|
42
67
|
if (!dryRun) {
|
|
43
|
-
for (const skill of
|
|
68
|
+
for (const skill of selected) {
|
|
44
69
|
const dst = path.join(target.skillsDir, skill)
|
|
45
70
|
fs.rmSync(dst, { recursive: true, force: true })
|
|
46
71
|
fs.mkdirSync(target.skillsDir, { recursive: true })
|
|
47
72
|
copyTree(path.join(pack.root, 'skills', skill), dst)
|
|
48
73
|
}
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
// A full install drops skills a previous version shipped but this one doesn't.
|
|
75
|
+
if (!partial) {
|
|
76
|
+
for (const old of previous?.skills ?? []) {
|
|
77
|
+
if (!pack.skills.includes(old)) fs.rmSync(path.join(target.skillsDir, old), { recursive: true, force: true })
|
|
78
|
+
}
|
|
52
79
|
}
|
|
53
80
|
}
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
// Partial installs add to what's already there.
|
|
83
|
+
const skills = partial ? pack.skills.filter((s) => selected.includes(s) || previous?.skills?.includes(s)) : pack.skills
|
|
84
|
+
const entry = { version: pack.version, skills, installedAt: new Date().toISOString() }
|
|
56
85
|
|
|
57
|
-
if (
|
|
86
|
+
if (wantsHook && target.hooks) {
|
|
58
87
|
const hookDir = path.join(target.hookRoot, pack.name)
|
|
59
88
|
const command = hookCommandFor(target, pack)
|
|
60
89
|
if (!dryRun) {
|
|
@@ -66,9 +95,11 @@ export function install(pack, opts = {}) {
|
|
|
66
95
|
}
|
|
67
96
|
addHook(target.settingsFile, { event: pack.hook.event, matcher: pack.hook.matcher, command })
|
|
68
97
|
}
|
|
69
|
-
entry.hook = { event: pack.hook.event, command, dir: hookDir, settingsFile: target.settingsFile }
|
|
98
|
+
entry.hook = { event: pack.hook.event, command, dir: hookDir, settingsFile: target.settingsFile, skills: hookSkills(pack) }
|
|
70
99
|
result.hook = entry.hook
|
|
71
|
-
} else if (
|
|
100
|
+
} else if (previous?.hook) {
|
|
101
|
+
entry.hook = previous.hook
|
|
102
|
+
} else if (wantsHook) {
|
|
72
103
|
result.note = `${target.label} has no hook system: skills installed, but the session-start bootstrap won't run automatically.`
|
|
73
104
|
}
|
|
74
105
|
|
|
@@ -80,7 +111,7 @@ export function install(pack, opts = {}) {
|
|
|
80
111
|
})
|
|
81
112
|
}
|
|
82
113
|
|
|
83
|
-
/** Remove exactly what the manifest says this pack installed. */
|
|
114
|
+
/** Remove exactly what the manifest says this pack installed (or, with `skills`, only those skills). */
|
|
84
115
|
export function uninstall(packName, opts = {}) {
|
|
85
116
|
const agents = expandAgents(opts.agent)
|
|
86
117
|
return agents.map((agent) => {
|
|
@@ -88,18 +119,27 @@ export function uninstall(packName, opts = {}) {
|
|
|
88
119
|
const manifest = readManifest(target.skillsDir)
|
|
89
120
|
const entry = manifest.packs[packName]
|
|
90
121
|
if (!entry) return { agent, label: target.label, removed: false }
|
|
122
|
+
|
|
123
|
+
const installed = entry.skills ?? []
|
|
124
|
+
const wanted = opts.skills?.length ? opts.skills : installed
|
|
125
|
+
const removing = installed.filter((s) => wanted.includes(s))
|
|
126
|
+
if (removing.length === 0) return { agent, label: target.label, removed: false }
|
|
127
|
+
const remaining = installed.filter((s) => !removing.includes(s))
|
|
128
|
+
// The hook goes with the last skill, or with the skill it injects.
|
|
129
|
+
const hook = entry.hook ?? null
|
|
130
|
+
const dropHook = Boolean(hook) && (remaining.length === 0 || (hook.skills ?? []).some((s) => removing.includes(s)))
|
|
131
|
+
|
|
91
132
|
if (!opts.dryRun) {
|
|
92
|
-
for (const s of
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
133
|
+
for (const s of removing) fs.rmSync(path.join(target.skillsDir, s), { recursive: true, force: true })
|
|
134
|
+
if (dropHook) removeHookFiles(hook)
|
|
135
|
+
if (remaining.length === 0) delete manifest.packs[packName]
|
|
136
|
+
else {
|
|
137
|
+
entry.skills = remaining
|
|
138
|
+
if (dropHook) delete entry.hook
|
|
98
139
|
}
|
|
99
|
-
delete manifest.packs[packName]
|
|
100
140
|
writeManifest(target.skillsDir, manifest)
|
|
101
141
|
}
|
|
102
|
-
return { agent, label: target.label, removed: true, skills:
|
|
142
|
+
return { agent, label: target.label, removed: true, skills: removing, partial: remaining.length > 0, hook: dropHook ? hook : null }
|
|
103
143
|
})
|
|
104
144
|
}
|
|
105
145
|
|
package/package.json
CHANGED