@lythos/skill-deck 0.9.28 → 0.9.30
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 +11 -11
- package/package.json +1 -1
- package/src/add.ts +21 -4
- package/src/cli.ts +4 -2
- package/src/link.test.ts +27 -5
- package/src/link.ts +52 -9
- package/src/refresh.ts +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
This package exposes a **CLI**. Invoke via:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
bunx @lythos/skill-deck@0.9.
|
|
12
|
+
bunx @lythos/skill-deck@0.9.30 <command> [options]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
No installation required. `bunx` auto-downloads the package.
|
|
@@ -55,14 +55,14 @@ prompt = "Search for latest info, then generate professional document with diagr
|
|
|
55
55
|
|
|
56
56
|
| Situation | Command |
|
|
57
57
|
|-----------|---------|
|
|
58
|
-
| Sync working set with `skill-deck.toml` | `bunx @lythos/skill-deck@0.9.
|
|
59
|
-
| Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck@0.9.
|
|
60
|
-
| Download a skill to cold pool and add to deck | `bunx @lythos/skill-deck@0.9.
|
|
61
|
-
| Pull latest versions of declared skills | `bunx @lythos/skill-deck@0.9.
|
|
62
|
-
| Refresh a single skill by alias | `bunx @lythos/skill-deck@0.9.
|
|
63
|
-
| Remove a skill from deck and working set | `bunx @lythos/skill-deck@0.9.
|
|
64
|
-
| GC unreferenced repos from cold pool | `bunx @lythos/skill-deck@0.9.
|
|
65
|
-
| Use a custom deck file or working dir | `bunx @lythos/skill-deck@0.9.
|
|
58
|
+
| Sync working set with `skill-deck.toml` | `bunx @lythos/skill-deck@0.9.30 link` |
|
|
59
|
+
| Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck@0.9.30 validate` |
|
|
60
|
+
| Download a skill to cold pool and add to deck | `bunx @lythos/skill-deck@0.9.30 add owner/repo` |
|
|
61
|
+
| Pull latest versions of declared skills | `bunx @lythos/skill-deck@0.9.30 refresh` |
|
|
62
|
+
| Refresh a single skill by alias | `bunx @lythos/skill-deck@0.9.30 refresh tdd` |
|
|
63
|
+
| Remove a skill from deck and working set | `bunx @lythos/skill-deck@0.9.30 remove tdd` |
|
|
64
|
+
| GC unreferenced repos from cold pool | `bunx @lythos/skill-deck@0.9.30 prune` |
|
|
65
|
+
| Use a custom deck file or working dir | `bunx @lythos/skill-deck@0.9.30 link --deck ./my-deck.toml --workdir /path/to/project` |
|
|
66
66
|
|
|
67
67
|
### Commands
|
|
68
68
|
|
|
@@ -119,7 +119,7 @@ path = "github.com/lythos-labs/lythoskill/skills/lythoskill-deck"
|
|
|
119
119
|
EOF
|
|
120
120
|
|
|
121
121
|
# 2. Link — creates symlinks in .claude/skills/
|
|
122
|
-
bunx @lythos/skill-deck@0.9.
|
|
122
|
+
bunx @lythos/skill-deck@0.9.30 link
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
### Key Concepts
|
|
@@ -148,7 +148,7 @@ Different agents look for skills in different directories. `skill-deck.toml` con
|
|
|
148
148
|
|
|
149
149
|
| Symptom | Cause | Fix |
|
|
150
150
|
|---------|-------|-----|
|
|
151
|
-
| `❌ Skill not found: <name>` | Skill declared in deck but not in cold pool | `bunx @lythos/skill-deck@0.9.
|
|
151
|
+
| `❌ Skill not found: <name>` | Skill declared in deck but not in cold pool | `bunx @lythos/skill-deck@0.9.30 add github.com/owner/repo/skill` or clone manually into cold pool |
|
|
152
152
|
| `link` skips entries with warnings | Real files/directories exist in working set (not symlinks) | Delete the real directories in `working_set` and re-run `link`. Never create directories manually there |
|
|
153
153
|
| `refresh` reports "Not a git repository" | Skill was copied (not cloned) into cold pool | Re-clone with `git clone` or use `deck add` which clones by default |
|
|
154
154
|
| `deck update` prints deprecation warning | `update` was renamed to `refresh` in v0.8+ | Use `deck refresh` instead |
|
package/package.json
CHANGED
package/src/add.ts
CHANGED
|
@@ -91,7 +91,7 @@ function exitInvalidLocator(locator: string): never {
|
|
|
91
91
|
|
|
92
92
|
export async function addSkill(
|
|
93
93
|
locator: string,
|
|
94
|
-
options: { deck?: string; workdir?: string; alias?: string; type?: string; dryRun?: boolean },
|
|
94
|
+
options: { deck?: string; workdir?: string; alias?: string; type?: string; dryRun?: boolean; mode?: 'symlink' | 'snapshot' },
|
|
95
95
|
) {
|
|
96
96
|
const dryRun = options.dryRun || false
|
|
97
97
|
const workdir = options.workdir ? resolvePath(options.workdir) : process.cwd()
|
|
@@ -245,15 +245,32 @@ export async function addSkill(
|
|
|
245
245
|
writeFileSync(deckPath, stringifyToml(deck))
|
|
246
246
|
console.log(`📝 Added "${alias}" to [${skillType}.skills] in ${deckPath}`)
|
|
247
247
|
} else {
|
|
248
|
-
const
|
|
248
|
+
const header = [
|
|
249
|
+
'# Skill Deck — generated by lythoskill-deck',
|
|
250
|
+
'# Edit working_set for your agent platform (uncomment one):',
|
|
251
|
+
'# working_set = ".claude/skills" # Claude Code (also read by Cursor, Copilot)',
|
|
252
|
+
'# working_set = ".agents/skills" # Codex CLI, OpenClaw',
|
|
253
|
+
'# working_set = ".cursor/skills" # Cursor-native',
|
|
254
|
+
'# working_set = ".github/skills" # GitHub Copilot',
|
|
255
|
+
'# working_set = ".windsurf/skills" # Windsurf',
|
|
256
|
+
'# After editing, run: bunx @lythos/skill-deck@latest link',
|
|
257
|
+
'',
|
|
258
|
+
].join('\n')
|
|
259
|
+
const minimal: Record<string, any> = {
|
|
260
|
+
deck: {
|
|
261
|
+
max_cards: 10,
|
|
262
|
+
cold_pool: '~/.agents/skill-repos',
|
|
263
|
+
working_set: '.claude/skills',
|
|
264
|
+
},
|
|
265
|
+
}
|
|
249
266
|
minimal[skillType] = { skills: { [alias]: { path: fqPath } } }
|
|
250
|
-
writeFileSync(deckPath, stringifyToml(minimal))
|
|
267
|
+
writeFileSync(deckPath, header + stringifyToml(minimal))
|
|
251
268
|
console.log(`📝 Created ${deckPath} with "${alias}"`)
|
|
252
269
|
}
|
|
253
270
|
|
|
254
271
|
console.log('🔗 Running deck link...')
|
|
255
272
|
const { linkDeck } = await import('./link.js')
|
|
256
|
-
linkDeck(deckPath, workdir)
|
|
273
|
+
await linkDeck(deckPath, workdir, { mode: options.mode })
|
|
257
274
|
|
|
258
275
|
// ── Metadata recording (content-level only; deck refs reconciled by link) ─
|
|
259
276
|
|
package/src/cli.ts
CHANGED
|
@@ -29,6 +29,7 @@ const noBackup = args.includes('--no-backup')
|
|
|
29
29
|
const yes = args.includes('--yes')
|
|
30
30
|
const dryRun = args.includes('--dry-run')
|
|
31
31
|
const remote = args.includes('--remote')
|
|
32
|
+
const mode = flagValue('--mode') as 'symlink' | 'snapshot' | undefined
|
|
32
33
|
|
|
33
34
|
const HELP_CONFIG = {
|
|
34
35
|
binName: 'lythoskill-deck',
|
|
@@ -45,6 +46,7 @@ const HELP_CONFIG = {
|
|
|
45
46
|
options: [
|
|
46
47
|
{ flag: '--deck <path>', description: 'Specify skill-deck.toml path (default: find upward from cwd)' },
|
|
47
48
|
{ flag: '--workdir <dir>', description: 'Specify working directory (default: cwd)' },
|
|
49
|
+
{ flag: '--mode <symlink|snapshot>', description: 'Link mode: symlink (default) or snapshot (cp)' },
|
|
48
50
|
{ flag: '--no-backup', description: 'Skip tar backup when removing non-symlink entries' },
|
|
49
51
|
|
|
50
52
|
{ flag: '--alias <name>', description: 'Explicit alias for the skill (default: basename of path)' },
|
|
@@ -62,7 +64,7 @@ switch (command) {
|
|
|
62
64
|
console.log(formatHelp(HELP_CONFIG))
|
|
63
65
|
process.exit(0)
|
|
64
66
|
case 'link':
|
|
65
|
-
linkDeck(deckPath, workdir, noBackup)
|
|
67
|
+
await linkDeck(deckPath, workdir, { noBackup, mode })
|
|
66
68
|
break
|
|
67
69
|
case 'add': {
|
|
68
70
|
const locator = args[1]
|
|
@@ -70,7 +72,7 @@ switch (command) {
|
|
|
70
72
|
console.error('❌ Missing locator. Usage: deck add <github.com/owner/repo[/skill]>')
|
|
71
73
|
process.exit(1)
|
|
72
74
|
}
|
|
73
|
-
await addSkill(locator, { deck: deckPath, workdir, alias, type, dryRun })
|
|
75
|
+
await addSkill(locator, { deck: deckPath, workdir, alias, type, dryRun, mode })
|
|
74
76
|
break
|
|
75
77
|
}
|
|
76
78
|
case 'refresh': {
|
package/src/link.test.ts
CHANGED
|
@@ -129,7 +129,7 @@ describe('linkDeck reconciler', () => {
|
|
|
129
129
|
const deckPath = join(projectDir, 'skill-deck.toml')
|
|
130
130
|
writeFileSync(deckPath, deckContent)
|
|
131
131
|
|
|
132
|
-
linkDeck(deckPath, projectDir, true)
|
|
132
|
+
linkDeck(deckPath, projectDir, { noBackup: true })
|
|
133
133
|
|
|
134
134
|
const workingSet = join(projectDir, '.claude', 'skills')
|
|
135
135
|
expect(existsSync(workingSet)).toBe(true)
|
|
@@ -167,7 +167,7 @@ describe('linkDeck reconciler', () => {
|
|
|
167
167
|
const deckPath = join(projectDir, 'skill-deck.toml')
|
|
168
168
|
writeFileSync(deckPath, deckContent)
|
|
169
169
|
|
|
170
|
-
linkDeck(deckPath, projectDir, true)
|
|
170
|
+
linkDeck(deckPath, projectDir, { noBackup: true })
|
|
171
171
|
|
|
172
172
|
const workingSet = join(projectDir, '.claude', 'skills')
|
|
173
173
|
const symlinkPath = join(workingSet, 'my-alias')
|
|
@@ -204,7 +204,7 @@ describe('linkDeck reconciler', () => {
|
|
|
204
204
|
const deckPath = join(projectDir, 'skill-deck.toml')
|
|
205
205
|
writeFileSync(deckPath, deckContent)
|
|
206
206
|
|
|
207
|
-
linkDeck(deckPath, projectDir, true)
|
|
207
|
+
linkDeck(deckPath, projectDir, { noBackup: true })
|
|
208
208
|
|
|
209
209
|
const lock1 = JSON.parse(readFileSync(join(projectDir, 'skill-deck.lock'), 'utf-8'))
|
|
210
210
|
const symlinkPath = join(projectDir, '.claude', 'skills', 'my-alias')
|
|
@@ -212,7 +212,7 @@ describe('linkDeck reconciler', () => {
|
|
|
212
212
|
|
|
213
213
|
await new Promise(r => setTimeout(r, 50))
|
|
214
214
|
|
|
215
|
-
linkDeck(deckPath, projectDir, true)
|
|
215
|
+
linkDeck(deckPath, projectDir, { noBackup: true })
|
|
216
216
|
|
|
217
217
|
const lock2 = JSON.parse(readFileSync(join(projectDir, 'skill-deck.lock'), 'utf-8'))
|
|
218
218
|
const target2 = readlinkSync(symlinkPath)
|
|
@@ -249,7 +249,7 @@ describe('linkDeck reconciler', () => {
|
|
|
249
249
|
symlinkSync(skillADir, join(workingSet, 'skill-a'))
|
|
250
250
|
symlinkSync(skillBDir, join(workingSet, 'skill-b'))
|
|
251
251
|
|
|
252
|
-
linkDeck(deckPath, projectDir, true)
|
|
252
|
+
linkDeck(deckPath, projectDir, { noBackup: true })
|
|
253
253
|
|
|
254
254
|
// skill-a should remain
|
|
255
255
|
expect(existsSync(join(workingSet, 'skill-a'))).toBe(true)
|
|
@@ -263,4 +263,26 @@ describe('linkDeck reconciler', () => {
|
|
|
263
263
|
expect(lock.skills[0].alias).toBe('skill-a')
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
+
it('snapshot mode: cp instead of symlink', () => {
|
|
267
|
+
const projectDir = makeTmp()
|
|
268
|
+
const coldPoolRel = 'cold-pool'
|
|
269
|
+
const coldPool = join(projectDir, coldPoolRel)
|
|
270
|
+
|
|
271
|
+
placeSkill(coldPool, 'github.com/owner/repo-a')
|
|
272
|
+
|
|
273
|
+
const deckContent = `[deck]\nmax_cards = 10\nworking_set = ".claude/skills"\ncold_pool = "${coldPoolRel}"\n\n[tool.skills.my-alias]\npath = "github.com/owner/repo-a"\n`
|
|
274
|
+
const deckPath = join(projectDir, 'skill-deck.toml')
|
|
275
|
+
writeFileSync(deckPath, deckContent)
|
|
276
|
+
|
|
277
|
+
linkDeck(deckPath, projectDir, { noBackup: true, mode: 'snapshot' })
|
|
278
|
+
|
|
279
|
+
const dest = join(projectDir, '.claude', 'skills', 'my-alias')
|
|
280
|
+
expect(existsSync(dest)).toBe(true)
|
|
281
|
+
// Snapshot: real directory, not a symlink
|
|
282
|
+
expect(lstatSync(dest).isDirectory()).toBe(true)
|
|
283
|
+
expect(lstatSync(dest).isSymbolicLink()).toBe(false)
|
|
284
|
+
// Verify content was copied
|
|
285
|
+
expect(existsSync(join(dest, 'SKILL.md'))).toBe(true)
|
|
286
|
+
})
|
|
287
|
+
|
|
266
288
|
})
|
package/src/link.ts
CHANGED
|
@@ -12,7 +12,7 @@ import YAML from "yaml";
|
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import {
|
|
14
14
|
existsSync, mkdirSync, readFileSync, readdirSync,
|
|
15
|
-
symlinkSync, lstatSync, rmSync, statSync, writeFileSync,
|
|
15
|
+
symlinkSync, cpSync, lstatSync, rmSync, statSync, writeFileSync,
|
|
16
16
|
} from "node:fs";
|
|
17
17
|
import { execFileSync } from "node:child_process";
|
|
18
18
|
import { resolve, dirname, join, basename, relative } from "node:path";
|
|
@@ -125,11 +125,37 @@ const BACKUP_SIZE_THRESHOLD = 100 * 1024 * 1024; // 100MB
|
|
|
125
125
|
|
|
126
126
|
// ── 主流程 ──────────────────────────────────────────────────
|
|
127
127
|
|
|
128
|
-
export function linkDeck(cliDeckPath?: string, cliWorkdir?: string, noBackup?: boolean): void {
|
|
128
|
+
export async function linkDeck(cliDeckPath?: string, cliWorkdir?: string, opts?: { noBackup?: boolean; mode?: 'symlink' | 'snapshot' }): Promise<void> {
|
|
129
|
+
const MODE = opts?.mode ?? 'symlink'
|
|
129
130
|
const cliDeck = cliDeckPath || process.argv.find((_, i, a) => a[i - 1] === "--deck");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
|
|
132
|
+
// If --deck is a URL, fetch it first (discovered via quick-agent.sh dogfooding)
|
|
133
|
+
let DECK_PATH: string
|
|
134
|
+
if (cliDeck && (cliDeck.startsWith('http://') || cliDeck.startsWith('https://'))) {
|
|
135
|
+
let url = cliDeck
|
|
136
|
+
if (url.includes('github.com/') && url.includes('/blob/')) {
|
|
137
|
+
url = url.replace('github.com/', 'raw.githubusercontent.com/').replace('/blob/', '/')
|
|
138
|
+
}
|
|
139
|
+
const dest = resolve(process.cwd(), 'skill-deck.toml')
|
|
140
|
+
console.log(`📥 Fetching deck: ${url}`)
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(30_000) })
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
console.error(`❌ Failed to fetch deck (HTTP ${res.status}): ${url}`)
|
|
145
|
+
process.exit(1)
|
|
146
|
+
}
|
|
147
|
+
writeFileSync(dest, await res.text())
|
|
148
|
+
console.log(` → saved to ${dest}`)
|
|
149
|
+
DECK_PATH = dest
|
|
150
|
+
} catch (e: any) {
|
|
151
|
+
console.error(`❌ Failed to fetch deck: ${e.message || e}`)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
DECK_PATH = cliDeck
|
|
156
|
+
? resolve(cliDeck)
|
|
157
|
+
: findDeckToml(process.cwd()) || resolve("skill-deck.toml");
|
|
158
|
+
}
|
|
133
159
|
|
|
134
160
|
if (!existsSync(DECK_PATH)) {
|
|
135
161
|
console.error(`❌ skill-deck.toml not found in ${process.cwd()}`);
|
|
@@ -144,7 +170,14 @@ if (!existsSync(DECK_PATH)) {
|
|
|
144
170
|
process.exit(1);
|
|
145
171
|
}
|
|
146
172
|
|
|
147
|
-
|
|
173
|
+
// --workdir always wins. --deck (explicit) defaults to cwd: user expects
|
|
174
|
+
// "work here" when pointing to a deck outside the current directory.
|
|
175
|
+
// Default (no flags): deck file's directory (99% of cases = project root).
|
|
176
|
+
const PROJECT_DIR = cliWorkdir
|
|
177
|
+
? resolve(cliWorkdir)
|
|
178
|
+
: cliDeck
|
|
179
|
+
? process.cwd()
|
|
180
|
+
: dirname(DECK_PATH);
|
|
148
181
|
const deckRaw = readFileSync(DECK_PATH, "utf-8");
|
|
149
182
|
const deckHash = hashContent(deckRaw);
|
|
150
183
|
|
|
@@ -333,14 +366,14 @@ if (nonSymlinks.length > 0) {
|
|
|
333
366
|
totalSize += calculateDirSize(join(WORKING_SET, e));
|
|
334
367
|
}
|
|
335
368
|
|
|
336
|
-
if (!noBackup && totalSize > BACKUP_SIZE_THRESHOLD) {
|
|
369
|
+
if (!opts?.noBackup && totalSize > BACKUP_SIZE_THRESHOLD) {
|
|
337
370
|
console.error(`❌ Found ${nonSymlinks.length} real directories in ${relative(PROJECT_DIR, WORKING_SET)} (> 100MB total).`);
|
|
338
371
|
console.error(` Manual review required: ${nonSymlinks.join(", ")}`);
|
|
339
372
|
console.error(` Use --no-backup to skip backup (removes without saving), or clean up manually.`);
|
|
340
373
|
process.exit(1);
|
|
341
374
|
}
|
|
342
375
|
|
|
343
|
-
if (!noBackup) {
|
|
376
|
+
if (!opts?.noBackup) {
|
|
344
377
|
const bakName = `skills.bak.${formatBackupDate(new Date())}.tar.gz`;
|
|
345
378
|
const bakPath = join(PROJECT_DIR, ".claude", bakName);
|
|
346
379
|
mkdirSync(join(PROJECT_DIR, ".claude"), { recursive: true });
|
|
@@ -400,7 +433,11 @@ for (const item of declared) {
|
|
|
400
433
|
|
|
401
434
|
try {
|
|
402
435
|
mkdirSync(dirname(dest), { recursive: true });
|
|
403
|
-
|
|
436
|
+
if (MODE === 'snapshot') {
|
|
437
|
+
cpSync(item.sourcePath, dest, { recursive: true });
|
|
438
|
+
} else {
|
|
439
|
+
symlinkSync(item.sourcePath, dest);
|
|
440
|
+
}
|
|
404
441
|
} catch (err: any) {
|
|
405
442
|
console.error(`❌ Link failed: ${item.alias}: ${err.message}`);
|
|
406
443
|
continue;
|
|
@@ -540,6 +577,12 @@ try {
|
|
|
540
577
|
// ── 报告 ────────────────────────────────────────────────────
|
|
541
578
|
|
|
542
579
|
console.log("");
|
|
580
|
+
console.log(`📋 deck: ${DECK_PATH}`);
|
|
581
|
+
console.log(`📁 working_set: ${resolvedWorkingSet}`);
|
|
582
|
+
console.log(`🗄️ cold_pool: ${COLD_POOL}`);
|
|
583
|
+
if (!cliWorkdir && cliDeck && dirname(DECK_PATH) !== process.cwd()) {
|
|
584
|
+
console.log(`💡 working_set 相对于当前目录。若期望跟随 deck 文件位置,使用 --workdir <dir>`);
|
|
585
|
+
}
|
|
543
586
|
console.log(`✅ Sync complete: ${linkedSkills.length} skill(s) linked (max_cards: ${MAX_CARDS})`);
|
|
544
587
|
console.log(` lock: ${LOCK_PATH}`);
|
|
545
588
|
if (dirOverlaps.length > 0) {
|
package/src/refresh.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function refreshDeck(cliDeckPath?: string, cliWorkdir?: string, target?:
|
|
|
66
66
|
linkDeck: () => {
|
|
67
67
|
console.log(`\n💡 Run 'bunx @lythos/skill-deck link' to sync refreshed skills to working set.`)
|
|
68
68
|
console.log('🔗 Running deck link...')
|
|
69
|
-
linkDeck(cliDeckPath, cliWorkdir)
|
|
69
|
+
await linkDeck(cliDeckPath, cliWorkdir)
|
|
70
70
|
},
|
|
71
71
|
})
|
|
72
72
|
|