@lythos/skill-deck 0.9.3 → 0.9.13
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/README.md +28 -4
- package/package.json +1 -1
- package/src/add.test.ts +1 -1
- package/src/add.ts +8 -48
- package/src/cli.ts +5 -7
- package/src/link.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lythos/skill-deck
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+
   
|
|
4
4
|
|
|
5
5
|
> Declarative skill deck governance. Reconcile declared skills against your cold pool via symlinks — deny-by-default, max-cards budgeting, transient expiry.
|
|
6
6
|
|
|
@@ -68,7 +68,7 @@ expires = "2026-05-01" # ISO date; warns at ≤14 days
|
|
|
68
68
|
|---------|------|-------------|
|
|
69
69
|
| `link` | `[--deck <path>] [--workdir <dir>]` | Sync working set. Removes undeclared skills (deny-by-default). |
|
|
70
70
|
| `validate` | `[deck.toml] [--workdir <dir>]` | Validate deck config without modifying files. |
|
|
71
|
-
| `add` | `<locator> [--
|
|
71
|
+
| `add` | `<locator> [--alias <alias>] [--type <type>] [--deck <path>]` | Git clone skill to cold pool and append to skill-deck.toml. |
|
|
72
72
|
| `refresh` | `[<fq|alias>] [--deck <path>]` | Pull latest versions of declared skills from upstream git repos. Pass a name to refresh one skill. |
|
|
73
73
|
| `remove` | `<fq|alias> [--deck <path>]` | Remove skill from deck.toml and working set. Cold pool untouched. |
|
|
74
74
|
| `prune` | `[--yes] [--deck <path>]` | GC cold pool repos no longer referenced. Interactive confirm (skip with `--yes`). |
|
|
@@ -79,8 +79,8 @@ expires = "2026-05-01" # ISO date; warns at ≤14 days
|
|
|
79
79
|
|------|-------------|---------|
|
|
80
80
|
| `--deck <path>` | Path to skill-deck.toml | Find upward from cwd |
|
|
81
81
|
| `--workdir <dir>` | Working directory | cwd |
|
|
82
|
-
|
|
83
|
-
| `--
|
|
82
|
+
|
|
83
|
+
| `--alias <alias>` | Explicit alias for the skill (default: basename of path) | — |
|
|
84
84
|
| `--type <type>` | Target section for `add`: `innate`, `tool`, or `combo` | `tool` |
|
|
85
85
|
|
|
86
86
|
### Safety guards
|
|
@@ -157,6 +157,30 @@ Different agents look for skills in different directories. `skill-deck.toml` con
|
|
|
157
157
|
| `deck add` fails with 404 | Locator format wrong or repo doesn't exist | Format: `github.com/owner/repo/skill-name` (path to skill directory inside repo) |
|
|
158
158
|
| `skill-deck.toml not found` | Running `link` outside project tree | Run from project root, or use `--deck ./path/to/skill-deck.toml` |
|
|
159
159
|
|
|
160
|
+
## Architecture: Intent / Plan / Execute
|
|
161
|
+
|
|
162
|
+
Deck commands separate pure logic from IO:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
deck.toml → RefreshPlan / PrunePlan (pure) → execute with injectable IO
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- **Plan**: `buildRefreshPlan()`, `buildPrunePlan()` — pure functions, unit-testable
|
|
169
|
+
- **Execute**: `executeRefreshPlan(plan, io)`, `executePrunePlan(plan, io)` — IO injected (`gitPull`, `delete`, `log`)
|
|
170
|
+
- **Config**: `workdir`, `coldPool`, `deckPath` all accept explicit overrides, defaults are fallback
|
|
171
|
+
|
|
172
|
+
This enables testing without real git operations — inject mock `gitPull`, capture `log` output, assert expected behavior.
|
|
173
|
+
|
|
174
|
+
## Test Coverage
|
|
175
|
+
|
|
176
|
+
| Layer | Count | CI | Notes |
|
|
177
|
+
|-------|-------|----|-------|
|
|
178
|
+
| Unit tests | 71 | ✅ | Plan generation, link, add, remove, schema |
|
|
179
|
+
| CLI BDD | 21 | ✅ | End-to-end via real CLI invocations in tmpdir |
|
|
180
|
+
| Agent BDD | 5 | ❌ | Requires `claude -p` CLI; `.agent.test.ts` convention |
|
|
181
|
+
|
|
182
|
+
Coverage is honest — no gate, no inflation. Agent BDD scenarios run locally only.
|
|
183
|
+
|
|
160
184
|
## More Documentation
|
|
161
185
|
|
|
162
186
|
- **Skill layer** (agent-facing instructions):
|
package/package.json
CHANGED
package/src/add.test.ts
CHANGED
|
@@ -137,7 +137,7 @@ describe('addSkill', () => {
|
|
|
137
137
|
|
|
138
138
|
try {
|
|
139
139
|
const { addSkill } = await import('./add.ts')
|
|
140
|
-
await addSkill('github.com/owner/repo-b', { workdir: projectDir, deck: deckPath,
|
|
140
|
+
await addSkill('github.com/owner/repo-b', { workdir: projectDir, deck: deckPath, alias: 'foo' })
|
|
141
141
|
expect(false).toBe(true) // should not reach here
|
|
142
142
|
} catch (err: any) {
|
|
143
143
|
expect(exitCode).toBe(1)
|
package/src/add.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* deck-add.ts — Skill acquisition command
|
|
4
4
|
*
|
|
5
5
|
* Downloads a skill to the cold pool, updates skill-deck.toml, and links.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Single backend: git clone. For feed-based discovery with decision tracking,
|
|
7
|
+
* use curator add instead.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
|
|
@@ -16,7 +16,6 @@ import { parse as parseToml, stringify as stringifyToml } from '@iarna/toml'
|
|
|
16
16
|
import { findDeckToml, expandHome } from './link.js'
|
|
17
17
|
import { parseDeck } from './parse-deck.js'
|
|
18
18
|
|
|
19
|
-
const CLAUDE_SKILLS_DIR = join(homedir(), '.claude', 'skills')
|
|
20
19
|
|
|
21
20
|
interface ParsedLocator {
|
|
22
21
|
host: string
|
|
@@ -76,7 +75,7 @@ function resolvePath(p: string): string {
|
|
|
76
75
|
return resolve(p)
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
export async function addSkill(locator: string, options: {
|
|
78
|
+
export async function addSkill(locator: string, options: { deck?: string; workdir?: string; alias?: string; type?: string }) {
|
|
80
79
|
const workdir = options.workdir ? resolvePath(options.workdir) : process.cwd()
|
|
81
80
|
const deckPath = options.deck
|
|
82
81
|
? resolvePath(options.deck)
|
|
@@ -89,8 +88,6 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
89
88
|
process.exit(1)
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
const backend = options.via || 'git'
|
|
93
|
-
|
|
94
91
|
let coldPool = join(homedir(), '.agents', 'skill-repos')
|
|
95
92
|
if (existsSync(deckPath)) {
|
|
96
93
|
try {
|
|
@@ -117,47 +114,10 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
117
114
|
const tmpRepo = join(tmpDir, 'repo')
|
|
118
115
|
|
|
119
116
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log(`📦 Downloading via skills.sh: ${skillsShLocator}`)
|
|
125
|
-
|
|
126
|
-
// Snapshot existing directories in ~/.claude/skills/
|
|
127
|
-
const beforeDirs = existsSync(CLAUDE_SKILLS_DIR)
|
|
128
|
-
? new Set(readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true })
|
|
129
|
-
.filter(e => e.isDirectory())
|
|
130
|
-
.map(e => e.name))
|
|
131
|
-
: new Set<string>()
|
|
132
|
-
|
|
133
|
-
execFileSync('npx', ['skills', 'add', skillsShLocator, '-g'], { cwd: tmpDir, stdio: 'inherit' })
|
|
134
|
-
|
|
135
|
-
// Detect the newly installed directory
|
|
136
|
-
const afterDirs = existsSync(CLAUDE_SKILLS_DIR)
|
|
137
|
-
? readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true })
|
|
138
|
-
.filter(e => e.isDirectory())
|
|
139
|
-
.map(e => e.name)
|
|
140
|
-
: []
|
|
141
|
-
const newDirs = afterDirs.filter(d => !beforeDirs.has(d))
|
|
142
|
-
|
|
143
|
-
if (newDirs.length === 0) {
|
|
144
|
-
console.error(`❌ skills.sh installed nothing new to ~/.claude/skills/`)
|
|
145
|
-
console.error(` The skill may already be installed, or the install failed.`)
|
|
146
|
-
process.exit(1)
|
|
147
|
-
}
|
|
148
|
-
if (newDirs.length > 1) {
|
|
149
|
-
console.warn(`⚠️ Multiple new directories detected in ~/.claude/skills/`)
|
|
150
|
-
console.warn(` Using the first one: ${newDirs[0]}`)
|
|
151
|
-
}
|
|
152
|
-
const installedName = newDirs[0]
|
|
153
|
-
skillSourceDir = join(CLAUDE_SKILLS_DIR, installedName)
|
|
154
|
-
console.log(` Detected install: ${installedName}`)
|
|
155
|
-
} else {
|
|
156
|
-
const gitUrl = `https://${parsed.host}/${parsed.owner}/${parsed.repo}.git`
|
|
157
|
-
console.log(`📦 Cloning: ${gitUrl}`)
|
|
158
|
-
execFileSync('git', ['clone', '--depth', '1', gitUrl, tmpRepo], { stdio: 'inherit' })
|
|
159
|
-
skillSourceDir = tmpRepo
|
|
160
|
-
}
|
|
117
|
+
const gitUrl = `https://${parsed.host}/${parsed.owner}/${parsed.repo}.git`
|
|
118
|
+
console.log(`📦 Cloning: ${gitUrl}`)
|
|
119
|
+
execFileSync('git', ['clone', '--depth', '1', gitUrl, tmpRepo], { stdio: 'inherit' })
|
|
120
|
+
let skillSourceDir = tmpRepo
|
|
161
121
|
|
|
162
122
|
if (!existsSync(skillSourceDir)) {
|
|
163
123
|
console.error(`❌ Download failed: expected output not found at ${skillSourceDir}`)
|
|
@@ -175,7 +135,7 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
175
135
|
}
|
|
176
136
|
|
|
177
137
|
const skillName = parsed.skill ? basename(parsed.skill) : parsed.repo
|
|
178
|
-
const alias = options.
|
|
138
|
+
const alias = options.alias || skillName
|
|
179
139
|
const skillType = (options.type || 'tool').toLowerCase()
|
|
180
140
|
|
|
181
141
|
if (!['innate', 'tool', 'combo'].includes(skillType)) {
|
package/src/cli.ts
CHANGED
|
@@ -14,14 +14,12 @@ const command = args[0]
|
|
|
14
14
|
|
|
15
15
|
const deckFlagIdx = args.indexOf('--deck')
|
|
16
16
|
const workdirFlagIdx = args.indexOf('--workdir')
|
|
17
|
-
const
|
|
18
|
-
const asFlagIdx = args.indexOf('--as')
|
|
17
|
+
const aliasFlagIdx = args.indexOf('--alias')
|
|
19
18
|
const typeFlagIdx = args.indexOf('--type')
|
|
20
19
|
|
|
21
20
|
const deckPath = deckFlagIdx >= 0 ? args[deckFlagIdx + 1] : undefined
|
|
22
21
|
const workdir = workdirFlagIdx >= 0 ? args[workdirFlagIdx + 1] : undefined
|
|
23
|
-
const
|
|
24
|
-
const as = asFlagIdx >= 0 ? args[asFlagIdx + 1] : undefined
|
|
22
|
+
const alias = aliasFlagIdx >= 0 ? args[aliasFlagIdx + 1] : undefined
|
|
25
23
|
const type = typeFlagIdx >= 0 ? args[typeFlagIdx + 1] : undefined
|
|
26
24
|
const noBackup = args.includes('--no-backup')
|
|
27
25
|
const yes = args.includes('--yes')
|
|
@@ -42,8 +40,8 @@ const HELP_CONFIG = {
|
|
|
42
40
|
{ flag: '--deck <path>', description: 'Specify skill-deck.toml path (default: find upward from cwd)' },
|
|
43
41
|
{ flag: '--workdir <dir>', description: 'Specify working directory (default: cwd)' },
|
|
44
42
|
{ flag: '--no-backup', description: 'Skip tar backup when removing non-symlink entries' },
|
|
45
|
-
|
|
46
|
-
{ flag: '--
|
|
43
|
+
|
|
44
|
+
{ flag: '--alias <name>', description: 'Explicit alias for the skill (default: basename of path)' },
|
|
47
45
|
{ flag: '--type <type>', description: 'Target section: innate | tool | combo (default: tool)' },
|
|
48
46
|
{ flag: '--yes', description: 'Skip interactive confirmation (for prune)' },
|
|
49
47
|
],
|
|
@@ -63,7 +61,7 @@ switch (command) {
|
|
|
63
61
|
console.error('❌ Missing locator. Usage: deck add <github.com/owner/repo[/skill]>')
|
|
64
62
|
process.exit(1)
|
|
65
63
|
}
|
|
66
|
-
await addSkill(locator, {
|
|
64
|
+
await addSkill(locator, { deck: deckPath, workdir, alias, type })
|
|
67
65
|
break
|
|
68
66
|
}
|
|
69
67
|
case 'refresh': {
|
package/src/link.ts
CHANGED
|
@@ -244,7 +244,7 @@ for (const d of declared) {
|
|
|
244
244
|
for (const [alias, types] of aliasToTypes) {
|
|
245
245
|
if (types.length > 1) {
|
|
246
246
|
errors.push(
|
|
247
|
-
`Alias collision: "${alias}" appears in [${types.join('], [')}]. Use --
|
|
247
|
+
`Alias collision: "${alias}" appears in [${types.join('], [')}]. Use --alias to specify different aliases.`
|
|
248
248
|
);
|
|
249
249
|
}
|
|
250
250
|
}
|