@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 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.32 <command> [options]
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.32 link` |
59
- | Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck@0.9.32 validate` |
60
- | Download a skill to cold pool and add to deck | `bunx @lythos/skill-deck@0.9.32 add owner/repo` |
61
- | Pull latest versions of declared skills | `bunx @lythos/skill-deck@0.9.32 refresh` |
62
- | Refresh a single skill by alias | `bunx @lythos/skill-deck@0.9.32 refresh tdd` |
63
- | Remove a skill from deck and working set | `bunx @lythos/skill-deck@0.9.32 remove tdd` |
64
- | GC unreferenced repos from cold pool | `bunx @lythos/skill-deck@0.9.32 prune` |
65
- | Switch skill from snapshot to sync mode | `bunx @lythos/skill-deck@0.9.32 sync tdd` |
66
- | Switch skill from sync to snapshot mode | `bunx @lythos/skill-deck@0.9.32 freeze tdd` |
67
- | Check cold pool for drift vs lock file | `bunx @lythos/skill-deck@0.9.32 reconcile` |
68
- | Use a custom deck file or working dir | `bunx @lythos/skill-deck@0.9.32 link --deck ./my-deck.toml --workdir /path/to/project` |
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 (`/`). It also only removes **symlinks** from the working set — real files or directories are skipped with a warning.
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.32 link
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** | Symlinks only. Default: `.claude/skills/` — where agents scan for skills. |
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.32 add github.com/owner/repo/skill` or clone manually into cold pool |
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, leaves real files untouched.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lythos/skill-deck",
3
- "version": "0.9.32",
3
+ "version": "0.9.36",
4
4
  "description": "Declarative skill deck governance — cold pool, working set, deny-by-default",
5
5
  "keywords": [
6
6
  "ai-agent",
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 deckPath = flagValue('--deck')
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
- // 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}`)
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
- 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)
142
+ } else {
143
+ DECK_PATH = resolveDeckPathSync(cliDeck).path
153
144
  }
154
- } else {
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
- const DECK_PATH = cliDeckPath
59
- ? resolve(cliDeckPath)
60
- : findDeckToml(PROJECT_DIR) || resolve(PROJECT_DIR, "skill-deck.toml");
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 {