@lythos/skill-deck 0.9.27 → 0.9.29
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 +24 -6
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.29 <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.29 link` |
|
|
59
|
+
| Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck@0.9.29 validate` |
|
|
60
|
+
| Download a skill to cold pool and add to deck | `bunx @lythos/skill-deck@0.9.29 add owner/repo` |
|
|
61
|
+
| Pull latest versions of declared skills | `bunx @lythos/skill-deck@0.9.29 refresh` |
|
|
62
|
+
| Refresh a single skill by alias | `bunx @lythos/skill-deck@0.9.29 refresh tdd` |
|
|
63
|
+
| Remove a skill from deck and working set | `bunx @lythos/skill-deck@0.9.29 remove tdd` |
|
|
64
|
+
| GC unreferenced repos from cold pool | `bunx @lythos/skill-deck@0.9.29 prune` |
|
|
65
|
+
| Use a custom deck file or working dir | `bunx @lythos/skill-deck@0.9.29 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.29 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.29 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
|
+
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
|
+
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,7 +125,8 @@ 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 function linkDeck(cliDeckPath?: string, cliWorkdir?: string, opts?: { noBackup?: boolean; mode?: 'symlink' | 'snapshot' }): void {
|
|
129
|
+
const MODE = opts?.mode ?? 'symlink'
|
|
129
130
|
const cliDeck = cliDeckPath || process.argv.find((_, i, a) => a[i - 1] === "--deck");
|
|
130
131
|
const DECK_PATH = cliDeck
|
|
131
132
|
? resolve(cliDeck)
|
|
@@ -144,7 +145,14 @@ if (!existsSync(DECK_PATH)) {
|
|
|
144
145
|
process.exit(1);
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
// --workdir always wins. --deck (explicit) defaults to cwd: user expects
|
|
149
|
+
// "work here" when pointing to a deck outside the current directory.
|
|
150
|
+
// Default (no flags): deck file's directory (99% of cases = project root).
|
|
151
|
+
const PROJECT_DIR = cliWorkdir
|
|
152
|
+
? resolve(cliWorkdir)
|
|
153
|
+
: cliDeck
|
|
154
|
+
? process.cwd()
|
|
155
|
+
: dirname(DECK_PATH);
|
|
148
156
|
const deckRaw = readFileSync(DECK_PATH, "utf-8");
|
|
149
157
|
const deckHash = hashContent(deckRaw);
|
|
150
158
|
|
|
@@ -333,14 +341,14 @@ if (nonSymlinks.length > 0) {
|
|
|
333
341
|
totalSize += calculateDirSize(join(WORKING_SET, e));
|
|
334
342
|
}
|
|
335
343
|
|
|
336
|
-
if (!noBackup && totalSize > BACKUP_SIZE_THRESHOLD) {
|
|
344
|
+
if (!opts?.noBackup && totalSize > BACKUP_SIZE_THRESHOLD) {
|
|
337
345
|
console.error(`❌ Found ${nonSymlinks.length} real directories in ${relative(PROJECT_DIR, WORKING_SET)} (> 100MB total).`);
|
|
338
346
|
console.error(` Manual review required: ${nonSymlinks.join(", ")}`);
|
|
339
347
|
console.error(` Use --no-backup to skip backup (removes without saving), or clean up manually.`);
|
|
340
348
|
process.exit(1);
|
|
341
349
|
}
|
|
342
350
|
|
|
343
|
-
if (!noBackup) {
|
|
351
|
+
if (!opts?.noBackup) {
|
|
344
352
|
const bakName = `skills.bak.${formatBackupDate(new Date())}.tar.gz`;
|
|
345
353
|
const bakPath = join(PROJECT_DIR, ".claude", bakName);
|
|
346
354
|
mkdirSync(join(PROJECT_DIR, ".claude"), { recursive: true });
|
|
@@ -400,7 +408,11 @@ for (const item of declared) {
|
|
|
400
408
|
|
|
401
409
|
try {
|
|
402
410
|
mkdirSync(dirname(dest), { recursive: true });
|
|
403
|
-
|
|
411
|
+
if (MODE === 'snapshot') {
|
|
412
|
+
cpSync(item.sourcePath, dest, { recursive: true });
|
|
413
|
+
} else {
|
|
414
|
+
symlinkSync(item.sourcePath, dest);
|
|
415
|
+
}
|
|
404
416
|
} catch (err: any) {
|
|
405
417
|
console.error(`❌ Link failed: ${item.alias}: ${err.message}`);
|
|
406
418
|
continue;
|
|
@@ -540,6 +552,12 @@ try {
|
|
|
540
552
|
// ── 报告 ────────────────────────────────────────────────────
|
|
541
553
|
|
|
542
554
|
console.log("");
|
|
555
|
+
console.log(`📋 deck: ${DECK_PATH}`);
|
|
556
|
+
console.log(`📁 working_set: ${resolvedWorkingSet}`);
|
|
557
|
+
console.log(`🗄️ cold_pool: ${COLD_POOL}`);
|
|
558
|
+
if (!cliWorkdir && cliDeck && dirname(DECK_PATH) !== process.cwd()) {
|
|
559
|
+
console.log(`💡 working_set 相对于当前目录。若期望跟随 deck 文件位置,使用 --workdir <dir>`);
|
|
560
|
+
}
|
|
543
561
|
console.log(`✅ Sync complete: ${linkedSkills.length} skill(s) linked (max_cards: ${MAX_CARDS})`);
|
|
544
562
|
console.log(` lock: ${LOCK_PATH}`);
|
|
545
563
|
if (dirOverlaps.length > 0) {
|