@lythos/skill-deck 0.6.2 โ 0.7.0
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 +8 -5
- package/package.json +1 -1
- package/src/add.ts +37 -9
- package/src/cli.ts +3 -1
- package/src/link.ts +127 -28
- package/src/validate.ts +6 -4
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ No installation required. `bunx` auto-downloads the package.
|
|
|
19
19
|
max_cards = 10
|
|
20
20
|
|
|
21
21
|
[tool]
|
|
22
|
-
skills = ["github.com/lythos-labs/lythoskill/lythoskill-deck"]
|
|
22
|
+
skills = ["github.com/lythos-labs/lythoskill/skills/lythoskill-deck"]
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
### skill-deck.toml (full reference)
|
|
@@ -31,13 +31,16 @@ working_set = ".claude/skills" # Where symlinks are created
|
|
|
31
31
|
cold_pool = "~/.agents/skill-repos" # Where skills are downloaded
|
|
32
32
|
|
|
33
33
|
[innate] # Always-loaded skills
|
|
34
|
-
skills = ["github.com/lythos-labs/lythoskill/lythoskill-deck"]
|
|
34
|
+
skills = ["github.com/lythos-labs/lythoskill/skills/lythoskill-deck"]
|
|
35
35
|
|
|
36
36
|
[tool] # Auto-triggered skills
|
|
37
|
-
skills = [
|
|
37
|
+
skills = [
|
|
38
|
+
"github.com/mattpocock/skills/skills/engineering/tdd",
|
|
39
|
+
"github.com/garrytan/gstack",
|
|
40
|
+
]
|
|
38
41
|
|
|
39
42
|
[combo] # Multi-skill bundles
|
|
40
|
-
skills = ["
|
|
43
|
+
skills = ["github.com/anthropics/skills/skills/pdf"]
|
|
41
44
|
|
|
42
45
|
[transient] # Temporary skills with expiry
|
|
43
46
|
[transient.handoff]
|
|
@@ -100,7 +103,7 @@ cat > skill-deck.toml << 'EOF'
|
|
|
100
103
|
max_cards = 10
|
|
101
104
|
|
|
102
105
|
[tool]
|
|
103
|
-
skills = ["github.com/lythos-labs/lythoskill/lythoskill-deck"]
|
|
106
|
+
skills = ["github.com/lythos-labs/lythoskill/skills/lythoskill-deck"]
|
|
104
107
|
EOF
|
|
105
108
|
|
|
106
109
|
# 2. Link โ creates symlinks in .claude/skills/
|
package/package.json
CHANGED
package/src/add.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { execSync } from 'node:child_process'
|
|
|
15
15
|
import { parse as parseToml, stringify as stringifyToml } from '@iarna/toml'
|
|
16
16
|
import { findDeckToml, expandHome } from './link.js'
|
|
17
17
|
|
|
18
|
+
const CLAUDE_SKILLS_DIR = join(homedir(), '.claude', 'skills')
|
|
19
|
+
|
|
18
20
|
interface ParsedLocator {
|
|
19
21
|
host: string
|
|
20
22
|
owner: string
|
|
@@ -114,29 +116,55 @@ export async function addSkill(locator: string, options: { via?: string; deck?:
|
|
|
114
116
|
const tmpRepo = join(tmpDir, 'repo')
|
|
115
117
|
|
|
116
118
|
try {
|
|
119
|
+
let skillSourceDir: string
|
|
120
|
+
|
|
117
121
|
if (backend === 'skills.sh' || backend === 'vercel') {
|
|
118
122
|
const skillsShLocator = `${parsed.owner}/${parsed.repo}`
|
|
119
123
|
console.log(`๐ฆ Downloading via skills.sh: ${skillsShLocator}`)
|
|
124
|
+
|
|
125
|
+
// Snapshot existing directories in ~/.claude/skills/
|
|
126
|
+
const beforeDirs = existsSync(CLAUDE_SKILLS_DIR)
|
|
127
|
+
? new Set(readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true })
|
|
128
|
+
.filter(e => e.isDirectory())
|
|
129
|
+
.map(e => e.name))
|
|
130
|
+
: new Set<string>()
|
|
131
|
+
|
|
120
132
|
execSync(`npx skills add ${skillsShLocator} -g`, { cwd: tmpDir, stdio: 'inherit' })
|
|
133
|
+
|
|
134
|
+
// Detect the newly installed directory
|
|
135
|
+
const afterDirs = existsSync(CLAUDE_SKILLS_DIR)
|
|
136
|
+
? readdirSync(CLAUDE_SKILLS_DIR, { withFileTypes: true })
|
|
137
|
+
.filter(e => e.isDirectory())
|
|
138
|
+
.map(e => e.name)
|
|
139
|
+
: []
|
|
140
|
+
const newDirs = afterDirs.filter(d => !beforeDirs.has(d))
|
|
141
|
+
|
|
142
|
+
if (newDirs.length === 0) {
|
|
143
|
+
console.error(`โ skills.sh installed nothing new to ~/.claude/skills/`)
|
|
144
|
+
console.error(` The skill may already be installed, or the install failed.`)
|
|
145
|
+
process.exit(1)
|
|
146
|
+
}
|
|
147
|
+
if (newDirs.length > 1) {
|
|
148
|
+
console.warn(`โ ๏ธ Multiple new directories detected in ~/.claude/skills/`)
|
|
149
|
+
console.warn(` Using the first one: ${newDirs[0]}`)
|
|
150
|
+
}
|
|
151
|
+
const installedName = newDirs[0]
|
|
152
|
+
skillSourceDir = join(CLAUDE_SKILLS_DIR, installedName)
|
|
153
|
+
console.log(` Detected install: ${installedName}`)
|
|
121
154
|
} else {
|
|
122
155
|
const gitUrl = `https://${parsed.host}/${parsed.owner}/${parsed.repo}.git`
|
|
123
156
|
console.log(`๐ฆ Cloning: ${gitUrl}`)
|
|
124
157
|
execSync(`git clone --depth 1 ${gitUrl} ${tmpRepo}`, { stdio: 'inherit' })
|
|
158
|
+
skillSourceDir = tmpRepo
|
|
125
159
|
}
|
|
126
160
|
|
|
127
|
-
if (!existsSync(
|
|
128
|
-
|
|
129
|
-
console.error(`โ skills.sh backend installs globally, not to cold pool.`)
|
|
130
|
-
console.error(` Please manually place the skill at: ${targetDir}`)
|
|
131
|
-
console.error(` Or use: deck add ${locator} --via git`)
|
|
132
|
-
process.exit(1)
|
|
133
|
-
}
|
|
134
|
-
console.error(`โ Download failed: expected output not found at ${tmpRepo}`)
|
|
161
|
+
if (!existsSync(skillSourceDir)) {
|
|
162
|
+
console.error(`โ Download failed: expected output not found at ${skillSourceDir}`)
|
|
135
163
|
process.exit(1)
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
mkdirSync(dirname(targetDir), { recursive: true })
|
|
139
|
-
renameSync(
|
|
167
|
+
renameSync(skillSourceDir, targetDir)
|
|
140
168
|
|
|
141
169
|
const skillDir = findSkillDir(targetDir, parsed.skill)
|
|
142
170
|
if (!skillDir) {
|
package/src/cli.ts
CHANGED
|
@@ -15,6 +15,7 @@ const HELP_CONFIG = {
|
|
|
15
15
|
options: [
|
|
16
16
|
{ flag: '--deck <path>', description: 'Specify skill-deck.toml path (default: find upward from cwd)' },
|
|
17
17
|
{ flag: '--workdir <dir>', description: 'Specify working directory (default: cwd)' },
|
|
18
|
+
{ flag: '--no-backup', description: 'Skip tar backup when removing non-symlink entries' },
|
|
18
19
|
{ flag: '--via <backend>', description: 'Download backend: git (default) | skills.sh' },
|
|
19
20
|
],
|
|
20
21
|
}
|
|
@@ -29,6 +30,7 @@ const viaFlagIdx = args.indexOf('--via')
|
|
|
29
30
|
const deckPath = deckFlagIdx >= 0 ? args[deckFlagIdx + 1] : undefined
|
|
30
31
|
const workdir = workdirFlagIdx >= 0 ? args[workdirFlagIdx + 1] : undefined
|
|
31
32
|
const via = viaFlagIdx >= 0 ? args[viaFlagIdx + 1] : undefined
|
|
33
|
+
const noBackup = args.includes('--no-backup')
|
|
32
34
|
|
|
33
35
|
switch (command) {
|
|
34
36
|
case '--help':
|
|
@@ -36,7 +38,7 @@ switch (command) {
|
|
|
36
38
|
console.log(formatHelp(HELP_CONFIG))
|
|
37
39
|
process.exit(0)
|
|
38
40
|
case 'link':
|
|
39
|
-
linkDeck(deckPath, workdir)
|
|
41
|
+
linkDeck(deckPath, workdir, noBackup)
|
|
40
42
|
break
|
|
41
43
|
case 'add': {
|
|
42
44
|
const locator = args[1]
|
package/src/link.ts
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
import { parse as parseToml } from "@iarna/toml";
|
|
11
11
|
import YAML from "yaml";
|
|
12
|
-
import { createHash } from "crypto";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
13
|
import {
|
|
14
14
|
existsSync, mkdirSync, readFileSync, readdirSync,
|
|
15
|
-
symlinkSync, lstatSync, rmSync, writeFileSync,
|
|
16
|
-
} from "fs";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
15
|
+
symlinkSync, lstatSync, rmSync, statSync, writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { resolve, dirname, join, basename, relative } from "node:path";
|
|
19
|
+
import { homedir } from "node:os";
|
|
19
20
|
import {
|
|
20
21
|
SkillDeckLockSchema,
|
|
21
22
|
type SkillDeckLock, type LinkedSkill, type ConstraintReport,
|
|
@@ -51,7 +52,12 @@ function parseSkillFrontmatter(skillMdPath: string): Record<string, any> {
|
|
|
51
52
|
|
|
52
53
|
// โโ ๅทๆฑ ๆฅๆพ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
53
54
|
|
|
54
|
-
export
|
|
55
|
+
export interface FindSourceResult {
|
|
56
|
+
path: string | null;
|
|
57
|
+
error?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function findSource(name: string, coldPool: string, projectDir: string): FindSourceResult {
|
|
55
61
|
// 0. Fully-qualified path: host.tld/owner/repo/skill
|
|
56
62
|
// โ cold_pool/host.tld/owner/repo/skills/skill
|
|
57
63
|
// Also handles host.tld/owner/repo (standalone skill without skills/ subdir)
|
|
@@ -65,31 +71,32 @@ export function findSource(name: string, coldPool: string, projectDir: string):
|
|
|
65
71
|
|
|
66
72
|
if (skill) {
|
|
67
73
|
const fqPath = join(coldPool, host, owner, repo, "skills", skill);
|
|
68
|
-
if (existsSync(join(fqPath, "SKILL.md"))) return fqPath;
|
|
74
|
+
if (existsSync(join(fqPath, "SKILL.md"))) return { path: fqPath };
|
|
69
75
|
}
|
|
70
76
|
// fallback: standalone skill at repo root
|
|
71
77
|
const directPath = join(coldPool, host, owner, repo);
|
|
72
|
-
if (existsSync(join(directPath, "SKILL.md"))) return directPath;
|
|
78
|
+
if (existsSync(join(directPath, "SKILL.md"))) return { path: directPath };
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
// 1. ็ดๆฅ่ทฏๅพ
|
|
76
82
|
const direct = resolve(coldPool, name);
|
|
77
|
-
if (existsSync(join(direct, "SKILL.md"))) return direct;
|
|
83
|
+
if (existsSync(join(direct, "SKILL.md"))) return { path: direct };
|
|
78
84
|
|
|
79
85
|
// 2. Monorepo: repo/skill โ cold_pool/repo/skills/skill
|
|
80
86
|
if (name.includes("/")) {
|
|
81
87
|
const [repo, ...rest] = name.split("/");
|
|
82
88
|
const mono = join(coldPool, repo, "skills", rest.join("/"));
|
|
83
|
-
if (existsSync(join(mono, "SKILL.md"))) return mono;
|
|
89
|
+
if (existsSync(join(mono, "SKILL.md"))) return { path: mono };
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
// 3. ้กน็ฎๆฌๅฐ: <project>/skills/<name>๏ผbuild ่พๅบ็ฎๅฝ๏ผไผๅ
็บง้ซไบๆๅนณๆซๆ๏ผ
|
|
87
93
|
const local = resolve(projectDir, "skills", name);
|
|
88
|
-
if (existsSync(join(local, "SKILL.md"))) return local;
|
|
94
|
+
if (existsSync(join(local, "SKILL.md"))) return { path: local };
|
|
89
95
|
|
|
90
96
|
// 4. ๆๅนณๆซๆ: cold_pool/<any-repo>/<name> ๆ <any-repo>/skills/<name>
|
|
91
97
|
// ่ทณ่ฟ้่็ฎๅฝ๏ผagent working setใgitใ้
็ฝฎ็ญ๏ผๅ node_modules๏ผ
|
|
92
98
|
// ้ฟๅ
ๆ .claude/skills/ ้็ symlink ่ฏฏๅคไธบๆๆ cold-pool ๆบ
|
|
99
|
+
const matches: string[] = [];
|
|
93
100
|
try {
|
|
94
101
|
for (const entry of readdirSync(coldPool, { withFileTypes: true })) {
|
|
95
102
|
if (!entry.isDirectory()) continue;
|
|
@@ -97,17 +104,54 @@ export function findSource(name: string, coldPool: string, projectDir: string):
|
|
|
97
104
|
if (entry.name === 'node_modules') continue;
|
|
98
105
|
const base = join(coldPool, entry.name);
|
|
99
106
|
for (const sub of [join(base, name), join(base, "skills", name)]) {
|
|
100
|
-
if (existsSync(join(sub, "SKILL.md")))
|
|
107
|
+
if (existsSync(join(sub, "SKILL.md"))) {
|
|
108
|
+
matches.push(sub);
|
|
109
|
+
}
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
} catch {}
|
|
104
113
|
|
|
105
|
-
|
|
114
|
+
if (matches.length === 1) {
|
|
115
|
+
return { path: matches[0] };
|
|
116
|
+
}
|
|
117
|
+
if (matches.length > 1) {
|
|
118
|
+
const candidates = matches.map(m => relative(coldPool, m)).join(', ');
|
|
119
|
+
return {
|
|
120
|
+
path: null,
|
|
121
|
+
error: `Ambiguous skill name "${name}": found ${matches.length} matches (${candidates}). Use fully-qualified name (e.g., github.com/owner/repo/${name})`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { path: null };
|
|
106
126
|
}
|
|
107
127
|
|
|
128
|
+
// โโ ๅคไปฝๅทฅๅ
ท โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
129
|
+
|
|
130
|
+
function calculateDirSize(dir: string): number {
|
|
131
|
+
let total = 0;
|
|
132
|
+
try {
|
|
133
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
134
|
+
const p = join(dir, entry.name);
|
|
135
|
+
if (entry.isDirectory()) {
|
|
136
|
+
total += calculateDirSize(p);
|
|
137
|
+
} else if (entry.isFile()) {
|
|
138
|
+
total += statSync(p).size;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {}
|
|
142
|
+
return total;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function formatBackupDate(d: Date): string {
|
|
146
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
147
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const BACKUP_SIZE_THRESHOLD = 100 * 1024 * 1024; // 100MB
|
|
151
|
+
|
|
108
152
|
// โโ ไธปๆต็จ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
109
153
|
|
|
110
|
-
export function linkDeck(cliDeckPath?: string, cliWorkdir?: string): void {
|
|
154
|
+
export function linkDeck(cliDeckPath?: string, cliWorkdir?: string, noBackup?: boolean): void {
|
|
111
155
|
const cliDeck = cliDeckPath || process.argv.find((_, i, a) => a[i - 1] === "--deck");
|
|
112
156
|
const DECK_PATH = cliDeck
|
|
113
157
|
? resolve(cliDeck)
|
|
@@ -150,12 +194,16 @@ const errors: string[] = [];
|
|
|
150
194
|
for (const section of ["innate", "tool", "combo"] as const) {
|
|
151
195
|
for (const name of (deck[section]?.skills || [])) {
|
|
152
196
|
if (!name || typeof name !== "string") continue;
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
197
|
+
const result = findSource(name, COLD_POOL, PROJECT_DIR);
|
|
198
|
+
if (result.error) {
|
|
199
|
+
errors.push(result.error);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!result.path) {
|
|
155
203
|
errors.push(`Skill not found: ${name}`);
|
|
156
204
|
continue;
|
|
157
205
|
}
|
|
158
|
-
declared.push({ name, type: section, sourcePath:
|
|
206
|
+
declared.push({ name, type: section, sourcePath: result.path });
|
|
159
207
|
}
|
|
160
208
|
}
|
|
161
209
|
|
|
@@ -239,23 +287,74 @@ if (
|
|
|
239
287
|
|
|
240
288
|
mkdirSync(WORKING_SET, { recursive: true });
|
|
241
289
|
|
|
242
|
-
//
|
|
290
|
+
// Pre-flight: ๅคไปฝๅนถๆธ
็้ symlink ๅฎไฝ๏ผ็ๅฎ็ฎๅฝ/ๆไปถ๏ผ
|
|
291
|
+
const nonSymlinks: string[] = [];
|
|
292
|
+
try {
|
|
293
|
+
for (const entry of readdirSync(WORKING_SET)) {
|
|
294
|
+
if (entry.startsWith("_") || entry.startsWith(".")) continue;
|
|
295
|
+
const entryPath = join(WORKING_SET, entry);
|
|
296
|
+
try {
|
|
297
|
+
const st = lstatSync(entryPath);
|
|
298
|
+
if (!st.isSymbolicLink()) {
|
|
299
|
+
nonSymlinks.push(entry);
|
|
300
|
+
}
|
|
301
|
+
} catch { continue; }
|
|
302
|
+
}
|
|
303
|
+
} catch {}
|
|
304
|
+
|
|
305
|
+
if (nonSymlinks.length > 0) {
|
|
306
|
+
// ่ฎก็ฎๆปๅคงๅฐ
|
|
307
|
+
let totalSize = 0;
|
|
308
|
+
for (const e of nonSymlinks) {
|
|
309
|
+
totalSize += calculateDirSize(join(WORKING_SET, e));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!noBackup && totalSize > BACKUP_SIZE_THRESHOLD) {
|
|
313
|
+
console.error(`โ Found ${nonSymlinks.length} real directories in ${relative(PROJECT_DIR, WORKING_SET)} (> 100MB total).`);
|
|
314
|
+
console.error(` Manual review required: ${nonSymlinks.join(", ")}`);
|
|
315
|
+
console.error(` Use --no-backup to skip backup (removes without saving), or clean up manually.`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!noBackup) {
|
|
320
|
+
const bakName = `skills.bak.${formatBackupDate(new Date())}.tar.gz`;
|
|
321
|
+
const bakPath = join(PROJECT_DIR, ".claude", bakName);
|
|
322
|
+
mkdirSync(join(PROJECT_DIR, ".claude"), { recursive: true });
|
|
323
|
+
|
|
324
|
+
const tarArgs = [
|
|
325
|
+
"czf", bakPath,
|
|
326
|
+
...nonSymlinks.map(e => relative(PROJECT_DIR, join(WORKING_SET, e))),
|
|
327
|
+
];
|
|
328
|
+
try {
|
|
329
|
+
execSync("tar " + tarArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" "), {
|
|
330
|
+
cwd: PROJECT_DIR,
|
|
331
|
+
stdio: "pipe",
|
|
332
|
+
});
|
|
333
|
+
console.log(`๐ฆ Backed up ${nonSymlinks.length} entr${nonSymlinks.length === 1 ? "y" : "ies"} to .claude/${bakName}`);
|
|
334
|
+
} catch (err: any) {
|
|
335
|
+
console.error(`โ Backup failed: ${err.message || err}`);
|
|
336
|
+
console.error(` Use --no-backup to skip backup, or fix the issue and retry.`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
console.log(`โ ๏ธ --no-backup: removing ${nonSymlinks.length} entr${nonSymlinks.length === 1 ? "y" : "ies"} without backup`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const e of nonSymlinks) {
|
|
344
|
+
rmSync(join(WORKING_SET, e), { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ๆธ
็ๆชๅฃฐๆ็ symlink
|
|
243
349
|
const declaredNames = new Set(declared.map(d => d.name.split("/")[0]));
|
|
244
350
|
try {
|
|
245
351
|
for (const entry of readdirSync(WORKING_SET)) {
|
|
246
|
-
if (entry.startsWith("_")) continue;
|
|
352
|
+
if (entry.startsWith("_") || entry.startsWith(".")) continue;
|
|
247
353
|
if (!declaredNames.has(entry)) {
|
|
248
354
|
const entryPath = join(WORKING_SET, entry);
|
|
249
355
|
try {
|
|
250
356
|
const st = lstatSync(entryPath);
|
|
251
|
-
if (!st.isSymbolicLink())
|
|
252
|
-
console.warn(`โ ๏ธ Skipping non-symlink entry: ${entry}`);
|
|
253
|
-
console.warn(` โ ${entry} is a real directory, not a symlink. Deck only manages symlinks.`);
|
|
254
|
-
const cpRel2 = relative(PROJECT_DIR, COLD_POOL);
|
|
255
|
-
const cpHint2 = cpRel2 === "" ? `skills/${entry}` : `${cpRel2}/${entry}`;
|
|
256
|
-
console.warn(` Move it to your cold pool (${cpHint2}) and run link again.`);
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
357
|
+
if (!st.isSymbolicLink()) continue; // ๅทฒๅจไธๆๅค็
|
|
259
358
|
} catch { continue; }
|
|
260
359
|
rmSync(entryPath, { recursive: true, force: true });
|
|
261
360
|
console.log(` ๐๏ธ Removed: ${entry}`);
|
|
@@ -399,7 +498,7 @@ writeFileSync(LOCK_PATH, JSON.stringify(parsed.data, null, 2) + "\n");
|
|
|
399
498
|
// โโ ๆฅๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
400
499
|
|
|
401
500
|
console.log("");
|
|
402
|
-
console.log(`โ
Sync complete: ${linkedSkills.length}
|
|
501
|
+
console.log(`โ
Sync complete: ${linkedSkills.length} skill(s) linked (max_cards: ${MAX_CARDS})`);
|
|
403
502
|
console.log(` lock: ${LOCK_PATH}`);
|
|
404
503
|
if (dirOverlaps.length > 0) {
|
|
405
504
|
console.log(` โ ๏ธ ${dirOverlaps.length} directory overlap(s) (see warnings above)`);
|
package/src/validate.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { parse as parseToml } from "@iarna/toml";
|
|
10
|
-
import { existsSync, readFileSync } from "fs";
|
|
11
|
-
import { resolve } from "path";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
12
|
import { findDeckToml, expandHome, findSource } from "./link.js";
|
|
13
13
|
|
|
14
14
|
export function validateDeck(cliDeckPath?: string, cliWorkdir?: string): void {
|
|
@@ -73,8 +73,10 @@ export function validateDeck(cliDeckPath?: string, cliWorkdir?: string): void {
|
|
|
73
73
|
}
|
|
74
74
|
declaredNames.add(name);
|
|
75
75
|
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
76
|
+
const result = findSource(name, COLD_POOL, PROJECT_DIR);
|
|
77
|
+
if (result.error) {
|
|
78
|
+
errors.push(result.error);
|
|
79
|
+
} else if (!result.path) {
|
|
78
80
|
errors.push(`Skill not found: ${name} (${section})`);
|
|
79
81
|
}
|
|
80
82
|
}
|