@lythos/skill-deck 0.9.32 → 0.9.36
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 +17 -17
- package/package.json +1 -1
- package/src/cli.ts +17 -1
- package/src/link.ts +11 -24
- package/src/resolve-deck.ts +55 -0
- package/src/validate.ts +12 -3
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.36 <command> [options]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
No installation required. `bunx` auto-downloads the package.
|
|
@@ -55,17 +55,17 @@ 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
|
-
| Switch skill from snapshot to sync mode | `bunx @lythos/skill-deck@0.9.
|
|
66
|
-
| Switch skill from sync to snapshot mode | `bunx @lythos/skill-deck@0.9.
|
|
67
|
-
| Check cold pool for drift vs lock file | `bunx @lythos/skill-deck@0.9.
|
|
68
|
-
| 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.36 link` |
|
|
59
|
+
| Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck@0.9.36 validate` |
|
|
60
|
+
| Download a skill to cold pool and add to deck | `bunx @lythos/skill-deck@0.9.36 add owner/repo` |
|
|
61
|
+
| Pull latest versions of declared skills | `bunx @lythos/skill-deck@0.9.36 refresh` |
|
|
62
|
+
| Refresh a single skill by alias | `bunx @lythos/skill-deck@0.9.36 refresh tdd` |
|
|
63
|
+
| Remove a skill from deck and working set | `bunx @lythos/skill-deck@0.9.36 remove tdd` |
|
|
64
|
+
| GC unreferenced repos from cold pool | `bunx @lythos/skill-deck@0.9.36 prune` |
|
|
65
|
+
| Switch skill from snapshot to sync mode | `bunx @lythos/skill-deck@0.9.36 sync tdd` |
|
|
66
|
+
| Switch skill from sync to snapshot mode | `bunx @lythos/skill-deck@0.9.36 freeze tdd` |
|
|
67
|
+
| Check cold pool for drift vs lock file | `bunx @lythos/skill-deck@0.9.36 reconcile` |
|
|
68
|
+
| Use a custom deck file or working dir | `bunx @lythos/skill-deck@0.9.36 link --deck ./my-deck.toml --workdir /path/to/project` |
|
|
69
69
|
|
|
70
70
|
### Commands
|
|
71
71
|
|
|
@@ -94,7 +94,7 @@ prompt = "Search for latest info, then generate professional document with diagr
|
|
|
94
94
|
|
|
95
95
|
### Safety guards
|
|
96
96
|
|
|
97
|
-
`link` refuses to operate if `working_set` resolves to your home directory or root (`/`).
|
|
97
|
+
`link` refuses to operate if `working_set` resolves to your home directory or root (`/`).
|
|
98
98
|
|
|
99
99
|
**Snapshot mode** (`--mode snapshot` or `link --mode snapshot`): copies the source directory into the working set instead of symlinking. This is needed for agents that don't support symlinks (e.g. Codex #11314). Snapshots are pinned to the cold pool version at link time. Use `deck sync <alias>` to switch back to live symlink mode, or `deck freeze <alias>` to pin a symlink as a snapshot.
|
|
100
100
|
|
|
@@ -128,7 +128,7 @@ path = "github.com/lythos-labs/lythoskill/skills/lythoskill-deck"
|
|
|
128
128
|
EOF
|
|
129
129
|
|
|
130
130
|
# 2. Link — creates symlinks in .claude/skills/
|
|
131
|
-
bunx @lythos/skill-deck@0.9.
|
|
131
|
+
bunx @lythos/skill-deck@0.9.36 link
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
### Key Concepts
|
|
@@ -138,7 +138,7 @@ bunx @lythos/skill-deck@0.9.32 link
|
|
|
138
138
|
| **Cold Pool** | All downloaded skills (`~/.agents/skill-repos/`). Agent cannot see here. |
|
|
139
139
|
| **skill-deck.toml** | Declares desired state: "this project uses these skills." |
|
|
140
140
|
| **`deck link`** | Reconciler. Makes the working set match the declaration. |
|
|
141
|
-
| **Working Set** |
|
|
141
|
+
| **Working Set** | Per-skill mode: symlink (live) or snapshot (pinned copy). Default: `.claude/skills/`. |
|
|
142
142
|
| **deny-by-default** | Undeclared skills are physically absent from the working set. |
|
|
143
143
|
|
|
144
144
|
### Agent skill scan locations
|
|
@@ -157,7 +157,7 @@ Different agents look for skills in different directories. `skill-deck.toml` con
|
|
|
157
157
|
|
|
158
158
|
| Symptom | Cause | Fix |
|
|
159
159
|
|---------|-------|-----|
|
|
160
|
-
| `❌ Skill not found: <name>` | Skill declared in deck but not in cold pool | `bunx @lythos/skill-deck@0.9.
|
|
160
|
+
| `❌ Skill not found: <name>` | Skill declared in deck but not in cold pool | `bunx @lythos/skill-deck@0.9.36 add github.com/owner/repo/skill` or clone manually into cold pool |
|
|
161
161
|
| `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 |
|
|
162
162
|
| `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 |
|
|
163
163
|
| `deck update` prints deprecation warning | `update` was renamed to `refresh` in v0.8+ | Use `deck refresh` instead |
|
|
@@ -222,7 +222,7 @@ If you already have skills installed (in working set, globally, or mixed), deck
|
|
|
222
222
|
3. BACKUP Always. `link` creates tar backups for non-symlink entries before removal.
|
|
223
223
|
Use `--no-backup` only if you're certain.
|
|
224
224
|
|
|
225
|
-
4. EXECUTE deck link — creates symlinks, removes undeclared
|
|
225
|
+
4. EXECUTE deck link — creates symlinks (default) or snapshots (--mode snapshot), removes undeclared entries.
|
|
226
226
|
|
|
227
227
|
5. VERIFY Agent checks: all declared skills resolve? Working set clean?
|
|
228
228
|
If unhappy: tar xf backup → rollback to pre-migration state.
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { removeSkill } from './remove.js'
|
|
|
9
9
|
import { pruneDeck } from './prune.js'
|
|
10
10
|
import { syncSkill, freezeSkill } from './sync-freeze.js'
|
|
11
11
|
import { reconcileDeck } from './reconcile.js'
|
|
12
|
+
import { resolveDeckPathSync, fetchDeckUrl, isUrl } from './resolve-deck.js'
|
|
12
13
|
import { formatHelp } from './help.js'
|
|
13
14
|
|
|
14
15
|
const args = process.argv.slice(2)
|
|
@@ -22,7 +23,22 @@ function flagValue(name: string): string | undefined {
|
|
|
22
23
|
return idx >= 0 ? args[idx + 1] : undefined
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const cliDeck = flagValue('--deck')
|
|
27
|
+
// URL deck: fetch first at the CLI dispatch layer, then pass local path to commands.
|
|
28
|
+
// Commands stay sync; URL I/O is handled once here.
|
|
29
|
+
let deckPath: string | undefined
|
|
30
|
+
if (cliDeck && isUrl(cliDeck)) {
|
|
31
|
+
try {
|
|
32
|
+
deckPath = await fetchDeckUrl(cliDeck)
|
|
33
|
+
} catch (e: any) {
|
|
34
|
+
console.error(`❌ ${e.message}`)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
} else if (cliDeck) {
|
|
38
|
+
deckPath = resolveDeckPathSync(cliDeck).path
|
|
39
|
+
} else {
|
|
40
|
+
deckPath = undefined
|
|
41
|
+
}
|
|
26
42
|
const workdir = flagValue('--workdir')
|
|
27
43
|
const alias = flagValue('--alias')
|
|
28
44
|
const type = flagValue('--type')
|
package/src/link.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type SkillDeckLock, type LinkedSkill, type ConstraintReport,
|
|
24
24
|
} from "./schema.js";
|
|
25
25
|
import { parseDeck } from "./parse-deck.js";
|
|
26
|
+
import { resolveDeckPathSync, fetchDeckUrl, isUrl } from "./resolve-deck.js";
|
|
26
27
|
|
|
27
28
|
// ── 路径工具 ────────────────────────────────────────────────
|
|
28
29
|
|
|
@@ -129,33 +130,19 @@ export async function linkDeck(cliDeckPath?: string, cliWorkdir?: string, opts?:
|
|
|
129
130
|
const MODE = opts?.mode ?? 'symlink'
|
|
130
131
|
const cliDeck = cliDeckPath || process.argv.find((_, i, a) => a[i - 1] === "--deck");
|
|
131
132
|
|
|
132
|
-
//
|
|
133
|
-
let DECK_PATH: string
|
|
134
|
-
if (cliDeck && (cliDeck
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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}`)
|
|
133
|
+
// URL deck: fetch first, then proceed as local
|
|
134
|
+
let DECK_PATH: string
|
|
135
|
+
if (cliDeck && isUrl(cliDeck)) {
|
|
136
|
+
try {
|
|
137
|
+
DECK_PATH = await fetchDeckUrl(cliDeck)
|
|
138
|
+
} catch (e: any) {
|
|
139
|
+
console.error(`❌ ${e.message}`)
|
|
145
140
|
process.exit(1)
|
|
146
141
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
DECK_PATH = dest
|
|
150
|
-
} catch (e: any) {
|
|
151
|
-
console.error(`❌ Failed to fetch deck: ${e.message || e}`)
|
|
152
|
-
process.exit(1)
|
|
142
|
+
} else {
|
|
143
|
+
DECK_PATH = resolveDeckPathSync(cliDeck).path
|
|
153
144
|
}
|
|
154
|
-
|
|
155
|
-
DECK_PATH = cliDeck
|
|
156
|
-
? resolve(cliDeck)
|
|
157
|
-
: findDeckToml(process.cwd()) || resolve("skill-deck.toml");
|
|
158
|
-
}
|
|
145
|
+
|
|
159
146
|
|
|
160
147
|
if (!existsSync(DECK_PATH)) {
|
|
161
148
|
console.error(`❌ skill-deck.toml not found in ${process.cwd()}`);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveDeckPath — shared deck URL/path resolution.
|
|
3
|
+
*
|
|
4
|
+
* Per ADR-20260508075301691: --deck accepts http/https URL.
|
|
5
|
+
* Extracted from link.ts so all commands (validate, add, refresh, etc.)
|
|
6
|
+
* inherit URL support without duplicating fetch logic.
|
|
7
|
+
*
|
|
8
|
+
* T1 of EPIC-20260508082810062 (Everything-from-URL).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { resolve } from 'node:path'
|
|
13
|
+
import { findDeckToml } from './link.js'
|
|
14
|
+
|
|
15
|
+
export interface ResolvedDeck {
|
|
16
|
+
path: string
|
|
17
|
+
source: 'url' | 'local' | 'default'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isUrl(s: string): boolean {
|
|
21
|
+
return s.startsWith('http://') || s.startsWith('https://')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeUrl(url: string): string {
|
|
25
|
+
try {
|
|
26
|
+
const u = new URL(url)
|
|
27
|
+
if (u.hostname === 'github.com' && u.pathname.includes('/blob/')) {
|
|
28
|
+
return `https://raw.githubusercontent.com${u.pathname.replace('/blob/', '/')}`
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
return url
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Sync: resolve local path or default. URL case handled separately. */
|
|
35
|
+
export function resolveDeckPathSync(cliArg?: string): ResolvedDeck {
|
|
36
|
+
if (cliArg) {
|
|
37
|
+
return { path: resolve(cliArg), source: 'local' }
|
|
38
|
+
}
|
|
39
|
+
const found = findDeckToml(process.cwd()) || resolve('skill-deck.toml')
|
|
40
|
+
return { path: found, source: 'default' }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Async: fetch URL deck, save to cwd, return local path. */
|
|
44
|
+
export async function fetchDeckUrl(url: string): Promise<string> {
|
|
45
|
+
const normalized = normalizeUrl(url)
|
|
46
|
+
const dest = resolve(process.cwd(), 'skill-deck.toml')
|
|
47
|
+
console.log(`📥 Fetching deck: ${normalized}`)
|
|
48
|
+
const res = await fetch(normalized, { signal: AbortSignal.timeout(30_000) })
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error(`Failed to fetch deck (HTTP ${res.status}): ${normalized}`)
|
|
51
|
+
}
|
|
52
|
+
writeFileSync(dest, await res.text())
|
|
53
|
+
console.log(` → saved to ${dest}`)
|
|
54
|
+
return dest
|
|
55
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { parse as parseToml } from "@iarna/toml";
|
|
14
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import { resolve } from "node:path";
|
|
16
|
+
import { resolveDeckPathSync, fetchDeckUrl, isUrl } from "./resolve-deck.js";
|
|
16
17
|
import {
|
|
17
18
|
buildValidationPlan,
|
|
18
19
|
executeValidationPlan,
|
|
@@ -55,9 +56,17 @@ export async function buildDeckValidation(
|
|
|
55
56
|
options: ValidateOptions = {},
|
|
56
57
|
): Promise<DeckValidationReport> {
|
|
57
58
|
const PROJECT_DIR = cliWorkdir ? resolve(cliWorkdir) : process.cwd();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
let DECK_PATH: string
|
|
60
|
+
if (cliDeckPath && isUrl(cliDeckPath)) {
|
|
61
|
+
try {
|
|
62
|
+
DECK_PATH = await fetchDeckUrl(cliDeckPath)
|
|
63
|
+
} catch (e: any) {
|
|
64
|
+
return { status: 'invalid', deckPath: cliDeckPath, errors: [`Failed to fetch deck: ${e.message}`], warnings: [], entries: [], skills: [], max_cards: 0, constraints: { total_cards: 0, within_budget: true, transient_warnings: [], dir_overlaps: [] } }
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const resolved = resolveDeckPathSync(cliDeckPath)
|
|
68
|
+
DECK_PATH = resolved.path
|
|
69
|
+
}
|
|
61
70
|
|
|
62
71
|
if (!existsSync(DECK_PATH)) {
|
|
63
72
|
return {
|