@lythos/skill-deck 0.7.0 → 0.7.2

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 CHANGED
@@ -55,6 +55,7 @@ skills = ["github.com/anthropics/skills/skills/pdf"]
55
55
  | Sync working set with `skill-deck.toml` | `bunx @lythos/skill-deck link` |
56
56
  | Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck validate` |
57
57
  | Download a skill to cold pool and add to deck | `bunx @lythos/skill-deck add owner/repo` |
58
+ | Pull latest versions of declared skills | `bunx @lythos/skill-deck update` |
58
59
  | Use a custom deck file or working dir | `bunx @lythos/skill-deck link --deck ./my-deck.toml --workdir /path/to/project` |
59
60
 
60
61
  ### Commands
@@ -64,6 +65,7 @@ skills = ["github.com/anthropics/skills/skills/pdf"]
64
65
  | `link` | `[--deck <path>] [--workdir <dir>]` | Sync working set. Removes undeclared skills (deny-by-default). |
65
66
  | `validate` | `[deck.toml] [--workdir <dir>]` | Validate deck config without modifying files. |
66
67
  | `add` | `<locator> [--via <backend>] [--deck <path>]` | Download skill to cold pool and append to skill-deck.toml. |
68
+ | `update` | `[--deck <path>]` | Pull latest versions of declared skills from upstream git repos. |
67
69
 
68
70
  ### Options
69
71
 
@@ -138,6 +140,7 @@ Different agents look for skills in different directories. `skill-deck.toml` con
138
140
  |---------|-------|-----|
139
141
  | `❌ Skill not found: <name>` | Skill declared in deck but not in cold pool | `bunx @lythos/skill-deck add github.com/owner/repo/skill` or clone manually into cold pool |
140
142
  | `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 |
143
+ | `update` 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 |
141
144
  | `link` refuses with "budget exceeded" | Declared skills > `max_cards` | Increase `max_cards` in `skill-deck.toml` or remove unused skills |
142
145
  | `link` refuses with "unsafe working_set" | `working_set` resolves to `~` or `/` | Check `skill-deck.toml` has correct relative path (e.g. `.claude/skills/`) |
143
146
  | Agent doesn't see skills after `link` | `working_set` path doesn't match agent's scan location | Claude Code: `.claude/skills/`; Cursor: `.cursor/skills/`; Kimi: check your platform docs. Set `working_set` correctly |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lythos/skill-deck",
3
- "version": "0.7.0",
4
- "description": "Declarative skill deck governance \u2014 cold pool, working set, deny-by-default",
3
+ "version": "0.7.2",
4
+ "description": "Declarative skill deck governance cold pool, working set, deny-by-default",
5
5
  "keywords": [
6
6
  "ai-agent",
7
7
  "skill",
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { linkDeck } from './link.js'
3
3
  import { validateDeck } from './validate.js'
4
4
  import { addSkill } from './add.js'
5
+ import { updateDeck } from './update.js'
5
6
  import { formatHelp } from './help.js'
6
7
 
7
8
  const HELP_CONFIG = {
@@ -10,6 +11,7 @@ const HELP_CONFIG = {
10
11
  commands: [
11
12
  { name: 'link', description: 'Sync working set with skill-deck.toml' },
12
13
  { name: 'add', description: 'Download skill to cold pool and add to deck', args: '<locator>' },
14
+ { name: 'update', description: 'Pull latest versions of declared skills from upstream' },
13
15
  { name: 'validate', description: 'Validate deck configuration', args: '[deck.toml]' },
14
16
  ],
15
17
  options: [
@@ -49,6 +51,9 @@ switch (command) {
49
51
  await addSkill(locator, { via, deck: deckPath, workdir })
50
52
  break
51
53
  }
54
+ case 'update':
55
+ updateDeck(deckPath, workdir)
56
+ break
52
57
  case 'validate':
53
58
  validateDeck(deckPath, workdir)
54
59
  break
package/src/link.ts CHANGED
@@ -175,8 +175,10 @@ const deckRaw = readFileSync(DECK_PATH, "utf-8");
175
175
  const deckHash = hashContent(deckRaw);
176
176
  const deck = parseToml(deckRaw) as any;
177
177
 
178
- const WORKING_SET = expandHome(deck.deck?.working_set || ".claude/skills", PROJECT_DIR);
179
- const COLD_POOL = expandHome(deck.deck?.cold_pool || "~/.agents/skill-repos", PROJECT_DIR);
178
+ const WORKING_SET_RAW = deck.deck?.working_set || ".claude/skills";
179
+ const COLD_POOL_RAW = deck.deck?.cold_pool || "~/.agents/skill-repos";
180
+ const WORKING_SET = expandHome(WORKING_SET_RAW, PROJECT_DIR);
181
+ const COLD_POOL = expandHome(COLD_POOL_RAW, PROJECT_DIR);
180
182
  const MAX_CARDS = Number(deck.deck?.max_cards || 10);
181
183
 
182
184
  // ── 收集声明 ────────────────────────────────────────────────
@@ -396,12 +398,17 @@ for (const item of declared) {
396
398
  contentHash = hashContent(readFileSync(skillMdPath, "utf-8"));
397
399
  } catch {}
398
400
 
401
+ // source: relative to cold_pool (non-transient) or project dir (transient)
402
+ const sourceRel = item.type === "transient"
403
+ ? relative(PROJECT_DIR, item.sourcePath)
404
+ : relative(COLD_POOL, item.sourcePath);
405
+
399
406
  linkedSkills.push({
400
407
  name: item.name,
401
408
  deck_niche: niche,
402
409
  type: item.type,
403
- source: item.sourcePath,
404
- dest,
410
+ source: sourceRel,
411
+ dest: relative(PROJECT_DIR, dest),
405
412
  content_hash: contentHash,
406
413
  linked_at: new Date().toISOString(),
407
414
  ...(item.expires ? { expires: item.expires } : {}),
@@ -479,9 +486,9 @@ const constraints: ConstraintReport = {
479
486
  const lock: SkillDeckLock = {
480
487
  version: "1.0.0",
481
488
  generated_at: new Date().toISOString(),
482
- deck_source: { path: DECK_PATH, content_hash: deckHash },
483
- working_set: WORKING_SET,
484
- cold_pool: COLD_POOL,
489
+ deck_source: { path: relative(PROJECT_DIR, DECK_PATH), content_hash: deckHash },
490
+ working_set: WORKING_SET_RAW,
491
+ cold_pool: COLD_POOL_RAW,
485
492
  skills: linkedSkills,
486
493
  constraints,
487
494
  };
package/src/update.ts ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * deck-update.ts — Update declared skills from their upstream sources
4
+ *
5
+ * 读取 skill-deck.toml → 遍历声明的 skill → 对 git 来源执行 pull。
6
+ * 职责:让冷池跟上上游版本。
7
+ * 不做:下载新 skill(那是 add 的职责)、修改 deck.toml、同步 working set。
8
+ */
9
+
10
+ import { parse as parseToml } from "@iarna/toml";
11
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
12
+ import { execSync } from "node:child_process";
13
+ import { resolve, dirname, join, relative } from "node:path";
14
+ import { findDeckToml, expandHome, findSource } from "./link.js";
15
+
16
+ interface UpdateResult {
17
+ name: string;
18
+ path: string;
19
+ status: "updated" | "up-to-date" | "skipped" | "failed" | "not-git";
20
+ message?: string;
21
+ }
22
+
23
+ function isGitRepo(dir: string): boolean {
24
+ return existsSync(join(dir, ".git"));
25
+ }
26
+
27
+ function gitPull(dir: string): { status: "updated" | "up-to-date" | "failed"; message: string } {
28
+ try {
29
+ const output = execSync("git pull", {
30
+ cwd: dir,
31
+ encoding: "utf-8",
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ timeout: 30000,
34
+ }).trim();
35
+
36
+ if (output.includes("Already up to date") || output.includes("Already up-to-date")) {
37
+ return { status: "up-to-date", message: output };
38
+ }
39
+ return { status: "updated", message: output };
40
+ } catch (err: any) {
41
+ const stderr = err.stderr?.toString() || err.message || "";
42
+ return { status: "failed", message: stderr.trim() };
43
+ }
44
+ }
45
+
46
+ export function updateDeck(cliDeckPath?: string, cliWorkdir?: string): void {
47
+ const cliDeck = cliDeckPath || process.argv.find((_, i, a) => a[i - 1] === "--deck");
48
+ const DECK_PATH = cliDeck
49
+ ? resolve(cliDeck)
50
+ : findDeckToml(process.cwd()) || resolve("skill-deck.toml");
51
+
52
+ if (!existsSync(DECK_PATH)) {
53
+ console.error(`❌ skill-deck.toml not found in ${process.cwd()}`);
54
+ console.error(`\nCreate one or specify a path: bunx @lythos/skill-deck link --deck /path/to/deck.toml`);
55
+ process.exit(1);
56
+ }
57
+
58
+ const PROJECT_DIR = cliWorkdir ? resolve(cliWorkdir) : dirname(DECK_PATH);
59
+ const deckRaw = readFileSync(DECK_PATH, "utf-8");
60
+ const deck = parseToml(deckRaw) as any;
61
+
62
+ const COLD_POOL = expandHome(deck.deck?.cold_pool || "~/.agents/skill-repos", PROJECT_DIR);
63
+
64
+ // ── 收集声明 ────────────────────────────────────────────────
65
+
66
+ const declared: { name: string; type: string }[] = [];
67
+
68
+ for (const section of ["innate", "tool", "combo"] as const) {
69
+ for (const name of (deck[section]?.skills || [])) {
70
+ if (!name || typeof name !== "string") continue;
71
+ declared.push({ name, type: section });
72
+ }
73
+ }
74
+
75
+ if (declared.length === 0) {
76
+ console.log("📭 No skills declared in deck. Nothing to update.");
77
+ process.exit(0);
78
+ }
79
+
80
+ // ── 执行更新 ────────────────────────────────────────────────
81
+
82
+ const results: UpdateResult[] = [];
83
+ let updated = 0;
84
+ let upToDate = 0;
85
+ let skipped = 0;
86
+ let failed = 0;
87
+
88
+ for (const { name, type } of declared) {
89
+ const result = findSource(name, COLD_POOL, PROJECT_DIR);
90
+
91
+ if (result.error || !result.path) {
92
+ results.push({ name, path: "", status: "failed", message: result.error || "Skill not found" });
93
+ failed++;
94
+ continue;
95
+ }
96
+
97
+ const path = result.path;
98
+
99
+ // localhost skills are user-managed; skip
100
+ const relativePath = relative(COLD_POOL, path);
101
+ if (relativePath.startsWith("localhost")) {
102
+ results.push({ name, path: relativePath, status: "skipped", message: "localhost skill — user-managed" });
103
+ skipped++;
104
+ continue;
105
+ }
106
+
107
+ if (!isGitRepo(path)) {
108
+ results.push({ name, path: relativePath, status: "not-git", message: "Not a git repository" });
109
+ skipped++;
110
+ continue;
111
+ }
112
+
113
+ const pullResult = gitPull(path);
114
+ results.push({ name, path: relativePath, status: pullResult.status, message: pullResult.message });
115
+
116
+ if (pullResult.status === "updated") updated++;
117
+ else if (pullResult.status === "up-to-date") upToDate++;
118
+ else failed++;
119
+ }
120
+
121
+ // ── 报告 ────────────────────────────────────────────────────
122
+
123
+ console.log(`\n📦 Skill Update Report — ${declared.length} skill(s) checked`);
124
+ console.log(` Updated: ${updated} | Up-to-date: ${upToDate} | Skipped: ${skipped} | Failed: ${failed}`);
125
+ console.log();
126
+
127
+ for (const r of results) {
128
+ const icon =
129
+ r.status === "updated" ? "🔄" :
130
+ r.status === "up-to-date" ? "✅" :
131
+ r.status === "skipped" ? "⏭️" :
132
+ r.status === "not-git" ? "📁" :
133
+ "❌";
134
+ console.log(`${icon} ${r.name}`);
135
+ if (r.message && r.status !== "up-to-date") {
136
+ const lines = r.message.split("\n").filter(l => l.trim());
137
+ for (const line of lines.slice(0, 3)) {
138
+ console.log(` ${line.trim()}`);
139
+ }
140
+ if (lines.length > 3) {
141
+ console.log(` ... (${lines.length - 3} more lines)`);
142
+ }
143
+ }
144
+ }
145
+
146
+ if (updated > 0) {
147
+ console.log(`\n💡 Run 'bunx @lythos/skill-deck link' to sync updated skills to working set.`);
148
+ }
149
+
150
+ if (failed > 0) {
151
+ process.exit(1);
152
+ }
153
+ }
154
+