skill-master 0.1.6 → 0.1.10
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 +1 -54
- package/README.zh-CN.md +17 -65
- package/dist/.metadata_never_index +0 -0
- package/dist/cli.js +796 -120
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/core/installer.ts
|
|
4
|
-
import { join as
|
|
4
|
+
import { join as join7, resolve as resolve3, relative } from "path";
|
|
5
5
|
import { existsSync as existsSync6 } from "fs";
|
|
6
6
|
|
|
7
7
|
// src/core/git-source.ts
|
|
@@ -145,6 +145,9 @@ function step(num, total, msg) {
|
|
|
145
145
|
function blank() {
|
|
146
146
|
console.log();
|
|
147
147
|
}
|
|
148
|
+
function section(title) {
|
|
149
|
+
console.log(chalk.bold(title));
|
|
150
|
+
}
|
|
148
151
|
function kv(key, value) {
|
|
149
152
|
console.log(` ${chalk.gray(key + ":")} ${value}`);
|
|
150
153
|
}
|
|
@@ -278,9 +281,123 @@ async function cloneRepo(url, branch) {
|
|
|
278
281
|
|
|
279
282
|
// src/core/skill-parser.ts
|
|
280
283
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
281
|
-
import { readdir } from "fs/promises";
|
|
282
|
-
import { join as
|
|
284
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
285
|
+
import { join as join3, basename, resolve as resolve2 } from "path";
|
|
283
286
|
import { existsSync as existsSync3 } from "fs";
|
|
287
|
+
|
|
288
|
+
// src/core/plugin-manifest.ts
|
|
289
|
+
import { readFile as readFile2, readdir } from "fs/promises";
|
|
290
|
+
import { join as join2, dirname as dirname2, resolve, normalize, sep } from "path";
|
|
291
|
+
function isContainedIn(targetPath, basePath) {
|
|
292
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
293
|
+
const normalizedTarget = normalize(resolve(targetPath));
|
|
294
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
295
|
+
}
|
|
296
|
+
function isValidRelativePath(path) {
|
|
297
|
+
return path.startsWith("./");
|
|
298
|
+
}
|
|
299
|
+
async function getPluginSkillPaths(basePath) {
|
|
300
|
+
const searchDirs = [];
|
|
301
|
+
const addPluginSkillPaths = (pluginBase, skills) => {
|
|
302
|
+
if (!isContainedIn(pluginBase, basePath)) return;
|
|
303
|
+
if (skills && skills.length > 0) {
|
|
304
|
+
for (const skillPath of skills) {
|
|
305
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
306
|
+
const skillDir = dirname2(join2(pluginBase, skillPath));
|
|
307
|
+
if (isContainedIn(skillDir, basePath)) {
|
|
308
|
+
searchDirs.push(skillDir);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
searchDirs.push(join2(pluginBase, "skills"));
|
|
313
|
+
};
|
|
314
|
+
try {
|
|
315
|
+
const content = await readFile2(join2(basePath, ".claude-plugin/marketplace.json"), "utf-8");
|
|
316
|
+
const manifest = JSON.parse(content);
|
|
317
|
+
const pluginRoot = manifest.metadata?.pluginRoot;
|
|
318
|
+
const validPluginRoot = pluginRoot === void 0 || isValidRelativePath(pluginRoot);
|
|
319
|
+
if (validPluginRoot) {
|
|
320
|
+
for (const plugin of manifest.plugins ?? []) {
|
|
321
|
+
if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
|
|
322
|
+
if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
|
|
323
|
+
const pluginBase = join2(basePath, pluginRoot ?? "", plugin.source ?? "");
|
|
324
|
+
addPluginSkillPaths(pluginBase, plugin.skills);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const content = await readFile2(join2(basePath, ".claude-plugin/plugin.json"), "utf-8");
|
|
331
|
+
const manifest = JSON.parse(content);
|
|
332
|
+
addPluginSkillPaths(basePath, manifest.skills);
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
return searchDirs;
|
|
336
|
+
}
|
|
337
|
+
async function scanSkillsDir(skillsDir, pluginName, basePath, groupings) {
|
|
338
|
+
try {
|
|
339
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
if (!entry.isDirectory()) continue;
|
|
342
|
+
const skillPath = join2(skillsDir, entry.name);
|
|
343
|
+
if (isContainedIn(skillPath, basePath)) {
|
|
344
|
+
groupings.set(resolve(skillPath), pluginName);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function getPluginGroupings(basePath) {
|
|
351
|
+
const groupings = /* @__PURE__ */ new Map();
|
|
352
|
+
try {
|
|
353
|
+
const content = await readFile2(join2(basePath, ".claude-plugin/marketplace.json"), "utf-8");
|
|
354
|
+
const manifest = JSON.parse(content);
|
|
355
|
+
const pluginRoot = manifest.metadata?.pluginRoot;
|
|
356
|
+
const validPluginRoot = pluginRoot === void 0 || isValidRelativePath(pluginRoot);
|
|
357
|
+
if (validPluginRoot) {
|
|
358
|
+
for (const plugin of manifest.plugins ?? []) {
|
|
359
|
+
if (!plugin.name) continue;
|
|
360
|
+
if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
|
|
361
|
+
if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
|
|
362
|
+
const pluginBase = join2(basePath, pluginRoot ?? "", plugin.source ?? "");
|
|
363
|
+
if (!isContainedIn(pluginBase, basePath)) continue;
|
|
364
|
+
if (plugin.skills && plugin.skills.length > 0) {
|
|
365
|
+
for (const skillPath of plugin.skills) {
|
|
366
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
367
|
+
const skillDir = join2(pluginBase, skillPath);
|
|
368
|
+
if (isContainedIn(skillDir, basePath)) {
|
|
369
|
+
groupings.set(resolve(skillDir), plugin.name);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
await scanSkillsDir(join2(pluginBase, "skills"), plugin.name, basePath, groupings);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const content = await readFile2(join2(basePath, ".claude-plugin/plugin.json"), "utf-8");
|
|
381
|
+
const manifest = JSON.parse(content);
|
|
382
|
+
if (manifest.name) {
|
|
383
|
+
if (manifest.skills && manifest.skills.length > 0) {
|
|
384
|
+
for (const skillPath of manifest.skills) {
|
|
385
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
386
|
+
const skillDir = join2(basePath, skillPath);
|
|
387
|
+
if (isContainedIn(skillDir, basePath)) {
|
|
388
|
+
groupings.set(resolve(skillDir), manifest.name);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
await scanSkillsDir(join2(basePath, "skills"), manifest.name, basePath, groupings);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
return groupings;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/core/skill-parser.ts
|
|
284
401
|
var TOOL_CAPABILITY_MAP = {
|
|
285
402
|
"Bash": "shell",
|
|
286
403
|
"Read": "read_file",
|
|
@@ -341,62 +458,108 @@ async function findSkillDirectory(dir) {
|
|
|
341
458
|
}
|
|
342
459
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"]);
|
|
343
460
|
async function findAllSkillDirectories(dir, fullDepth = false) {
|
|
461
|
+
const results = await findAllSkillDirectoriesWithPlugins(dir, fullDepth);
|
|
462
|
+
return results.map((r) => r.path);
|
|
463
|
+
}
|
|
464
|
+
async function findAllSkillDirectoriesWithPlugins(dir, fullDepth = false) {
|
|
465
|
+
const pluginGroupings = await getPluginGroupings(dir);
|
|
466
|
+
const enhance = (skillPath) => {
|
|
467
|
+
const resolvedPath = resolve2(skillPath);
|
|
468
|
+
const pluginName = pluginGroupings.get(resolvedPath);
|
|
469
|
+
return { path: skillPath, pluginName };
|
|
470
|
+
};
|
|
344
471
|
if (fullDepth) {
|
|
345
472
|
const results2 = /* @__PURE__ */ new Set();
|
|
346
473
|
await walkForSkills(dir, 0, 5, results2);
|
|
347
|
-
return [...results2];
|
|
474
|
+
return [...results2].map(enhance);
|
|
348
475
|
}
|
|
349
476
|
const results = [];
|
|
350
|
-
if (existsSync3(
|
|
351
|
-
results.push(dir);
|
|
477
|
+
if (existsSync3(join3(dir, "SKILL.md"))) {
|
|
478
|
+
results.push(enhance(dir));
|
|
352
479
|
return results;
|
|
353
480
|
}
|
|
354
|
-
const
|
|
355
|
-
|
|
481
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
482
|
+
const pluginSkillPaths = await getPluginSkillPaths(dir);
|
|
483
|
+
const priorityDirs = [
|
|
484
|
+
...pluginSkillPaths,
|
|
485
|
+
// Plugin manifest paths first
|
|
486
|
+
join3(dir, "skills"),
|
|
487
|
+
join3(dir, "skills", ".curated"),
|
|
488
|
+
join3(dir, "skills", ".experimental"),
|
|
489
|
+
join3(dir, "skills", ".system"),
|
|
490
|
+
join3(dir, ".agent", "skills"),
|
|
491
|
+
join3(dir, ".agents", "skills"),
|
|
492
|
+
join3(dir, ".claude", "skills"),
|
|
493
|
+
join3(dir, ".cline", "skills"),
|
|
494
|
+
join3(dir, ".codex", "skills"),
|
|
495
|
+
join3(dir, ".github", "skills"),
|
|
496
|
+
join3(dir, ".kiro", "skills"),
|
|
497
|
+
join3(dir, ".opencode", "skills"),
|
|
498
|
+
join3(dir, ".roo", "skills"),
|
|
499
|
+
join3(dir, ".windsurf", "skills")
|
|
500
|
+
];
|
|
501
|
+
for (const searchDir of priorityDirs) {
|
|
356
502
|
try {
|
|
357
|
-
const entries = await
|
|
503
|
+
const entries = await readdir2(searchDir, { withFileTypes: true });
|
|
358
504
|
for (const entry of entries) {
|
|
359
505
|
if (entry.isDirectory()) {
|
|
360
|
-
const
|
|
361
|
-
if (existsSync3(
|
|
362
|
-
results.push(
|
|
506
|
+
const skillDir = join3(searchDir, entry.name);
|
|
507
|
+
if (existsSync3(join3(skillDir, "SKILL.md")) && !seenPaths.has(skillDir)) {
|
|
508
|
+
results.push(enhance(skillDir));
|
|
509
|
+
seenPaths.add(skillDir);
|
|
363
510
|
}
|
|
364
511
|
}
|
|
365
512
|
}
|
|
366
513
|
} catch {
|
|
367
514
|
}
|
|
368
515
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
516
|
+
if (results.length === 0) {
|
|
517
|
+
try {
|
|
518
|
+
const topEntries = await readdir2(dir, { withFileTypes: true });
|
|
519
|
+
for (const entry of topEntries) {
|
|
520
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && !SKIP_DIRS.has(entry.name)) {
|
|
521
|
+
const subDir = join3(dir, entry.name);
|
|
522
|
+
if (existsSync3(join3(subDir, "SKILL.md")) && !seenPaths.has(subDir)) {
|
|
523
|
+
results.push(enhance(subDir));
|
|
524
|
+
seenPaths.add(subDir);
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const subEntries = await readdir2(subDir, { withFileTypes: true });
|
|
528
|
+
for (const sub of subEntries) {
|
|
529
|
+
if (sub.isDirectory() && !sub.name.startsWith(".") && !SKIP_DIRS.has(sub.name)) {
|
|
530
|
+
const nested = join3(subDir, sub.name);
|
|
531
|
+
if (existsSync3(join3(nested, "SKILL.md")) && !seenPaths.has(nested)) {
|
|
532
|
+
results.push(enhance(nested));
|
|
533
|
+
seenPaths.add(nested);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
376
539
|
}
|
|
377
540
|
}
|
|
541
|
+
} catch {
|
|
378
542
|
}
|
|
379
|
-
} catch {
|
|
380
543
|
}
|
|
381
544
|
return results;
|
|
382
545
|
}
|
|
383
546
|
async function walkForSkills(dir, depth, maxDepth, results) {
|
|
384
547
|
if (depth > maxDepth) return;
|
|
385
|
-
if (existsSync3(
|
|
548
|
+
if (existsSync3(join3(dir, "SKILL.md"))) {
|
|
386
549
|
results.add(dir);
|
|
387
550
|
}
|
|
388
551
|
try {
|
|
389
|
-
const entries = await
|
|
552
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
390
553
|
for (const entry of entries) {
|
|
391
554
|
if (entry.isDirectory() && !entry.name.startsWith(".") && !SKIP_DIRS.has(entry.name)) {
|
|
392
|
-
await walkForSkills(
|
|
555
|
+
await walkForSkills(join3(dir, entry.name), depth + 1, maxDepth, results);
|
|
393
556
|
}
|
|
394
557
|
}
|
|
395
558
|
} catch {
|
|
396
559
|
}
|
|
397
560
|
}
|
|
398
561
|
async function readSkillMd(dir) {
|
|
399
|
-
const content = await readTextSafe(
|
|
562
|
+
const content = await readTextSafe(join3(dir, "SKILL.md"));
|
|
400
563
|
if (!content) return null;
|
|
401
564
|
return parseSkillMd(content, basename(dir));
|
|
402
565
|
}
|
|
@@ -413,315 +576,364 @@ function extractEnvKeys(envExampleContent) {
|
|
|
413
576
|
}
|
|
414
577
|
return keys;
|
|
415
578
|
}
|
|
579
|
+
async function discoverNodeModulesSkills(cwd) {
|
|
580
|
+
const nmDir = join3(cwd, "node_modules");
|
|
581
|
+
if (!existsSync3(nmDir)) return [];
|
|
582
|
+
const results = [];
|
|
583
|
+
try {
|
|
584
|
+
const entries = await readdir2(nmDir, { withFileTypes: true });
|
|
585
|
+
for (const entry of entries) {
|
|
586
|
+
if (!entry.isDirectory()) continue;
|
|
587
|
+
if (entry.name.startsWith("@")) {
|
|
588
|
+
try {
|
|
589
|
+
const scopeEntries = await readdir2(join3(nmDir, entry.name), { withFileTypes: true });
|
|
590
|
+
for (const scopeEntry of scopeEntries) {
|
|
591
|
+
if (scopeEntry.isDirectory()) {
|
|
592
|
+
const pkgDir = join3(nmDir, entry.name, scopeEntry.name);
|
|
593
|
+
if (existsSync3(join3(pkgDir, "SKILL.md"))) {
|
|
594
|
+
results.push(pkgDir);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} catch {
|
|
599
|
+
}
|
|
600
|
+
} else if (!entry.name.startsWith(".")) {
|
|
601
|
+
const pkgDir = join3(nmDir, entry.name);
|
|
602
|
+
if (existsSync3(join3(pkgDir, "SKILL.md"))) {
|
|
603
|
+
results.push(pkgDir);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
}
|
|
609
|
+
return results;
|
|
610
|
+
}
|
|
416
611
|
|
|
417
612
|
// src/core/env-manager.ts
|
|
418
|
-
import { join as
|
|
613
|
+
import { join as join6 } from "path";
|
|
419
614
|
|
|
420
615
|
// src/utils/paths.ts
|
|
421
616
|
import { homedir as homedir2 } from "os";
|
|
422
|
-
import { join as
|
|
617
|
+
import { join as join5 } from "path";
|
|
423
618
|
|
|
424
619
|
// src/platform/agents.ts
|
|
425
620
|
import { existsSync as existsSync4 } from "fs";
|
|
426
|
-
import { join as
|
|
621
|
+
import { join as join4 } from "path";
|
|
427
622
|
import { homedir } from "os";
|
|
428
623
|
var home = homedir();
|
|
429
|
-
var configHome = process.env.XDG_CONFIG_HOME?.trim() ||
|
|
430
|
-
var codexHome = process.env.CODEX_HOME?.trim() ||
|
|
431
|
-
var claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() ||
|
|
624
|
+
var configHome = process.env.XDG_CONFIG_HOME?.trim() || join4(home, ".config");
|
|
625
|
+
var codexHome = process.env.CODEX_HOME?.trim() || join4(home, ".codex");
|
|
626
|
+
var claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join4(home, ".claude");
|
|
432
627
|
var AGENTS = {
|
|
433
628
|
amp: {
|
|
434
629
|
name: "amp",
|
|
435
630
|
displayName: "Amp",
|
|
436
631
|
skillsDir: ".agents/skills",
|
|
437
|
-
globalSkillsDir:
|
|
632
|
+
globalSkillsDir: join4(configHome, "agents/skills")
|
|
438
633
|
},
|
|
439
634
|
antigravity: {
|
|
440
635
|
name: "antigravity",
|
|
441
636
|
displayName: "Antigravity",
|
|
442
637
|
skillsDir: ".agent/skills",
|
|
443
|
-
globalSkillsDir:
|
|
638
|
+
globalSkillsDir: join4(home, ".gemini/antigravity/skills"),
|
|
444
639
|
detectMarker: ".agent"
|
|
445
640
|
},
|
|
446
641
|
augment: {
|
|
447
642
|
name: "augment",
|
|
448
643
|
displayName: "Augment",
|
|
449
644
|
skillsDir: ".augment/skills",
|
|
450
|
-
globalSkillsDir:
|
|
645
|
+
globalSkillsDir: join4(home, ".augment/skills"),
|
|
451
646
|
detectMarker: ".augment"
|
|
452
647
|
},
|
|
453
648
|
"claude-code": {
|
|
454
649
|
name: "claude-code",
|
|
455
650
|
displayName: "Claude Code",
|
|
456
651
|
skillsDir: ".claude/skills",
|
|
457
|
-
globalSkillsDir:
|
|
652
|
+
globalSkillsDir: join4(claudeHome, "skills"),
|
|
458
653
|
detectMarker: ".claude"
|
|
459
654
|
},
|
|
460
655
|
openclaw: {
|
|
461
656
|
name: "openclaw",
|
|
462
657
|
displayName: "OpenClaw",
|
|
463
658
|
skillsDir: "skills",
|
|
464
|
-
globalSkillsDir:
|
|
659
|
+
globalSkillsDir: join4(home, ".openclaw/skills")
|
|
465
660
|
},
|
|
466
661
|
cline: {
|
|
467
662
|
name: "cline",
|
|
468
663
|
displayName: "Cline",
|
|
469
|
-
skillsDir: ".
|
|
470
|
-
globalSkillsDir:
|
|
664
|
+
skillsDir: ".agents/skills",
|
|
665
|
+
globalSkillsDir: join4(home, ".agents", "skills"),
|
|
471
666
|
detectMarker: ".cline"
|
|
472
667
|
},
|
|
473
668
|
codebuddy: {
|
|
474
669
|
name: "codebuddy",
|
|
475
670
|
displayName: "CodeBuddy",
|
|
476
671
|
skillsDir: ".codebuddy/skills",
|
|
477
|
-
globalSkillsDir:
|
|
672
|
+
globalSkillsDir: join4(home, ".codebuddy/skills"),
|
|
478
673
|
detectMarker: ".codebuddy"
|
|
479
674
|
},
|
|
480
675
|
codex: {
|
|
481
676
|
name: "codex",
|
|
482
677
|
displayName: "Codex",
|
|
483
678
|
skillsDir: ".agents/skills",
|
|
484
|
-
globalSkillsDir:
|
|
679
|
+
globalSkillsDir: join4(codexHome, "skills")
|
|
485
680
|
},
|
|
486
681
|
"command-code": {
|
|
487
682
|
name: "command-code",
|
|
488
683
|
displayName: "Command Code",
|
|
489
684
|
skillsDir: ".commandcode/skills",
|
|
490
|
-
globalSkillsDir:
|
|
685
|
+
globalSkillsDir: join4(home, ".commandcode/skills"),
|
|
491
686
|
detectMarker: ".commandcode"
|
|
492
687
|
},
|
|
688
|
+
cortex: {
|
|
689
|
+
name: "cortex",
|
|
690
|
+
displayName: "Cortex",
|
|
691
|
+
skillsDir: ".cortex/skills",
|
|
692
|
+
globalSkillsDir: join4(home, ".snowflake/cortex/skills")
|
|
693
|
+
},
|
|
493
694
|
continue: {
|
|
494
695
|
name: "continue",
|
|
495
696
|
displayName: "Continue",
|
|
496
697
|
skillsDir: ".continue/skills",
|
|
497
|
-
globalSkillsDir:
|
|
698
|
+
globalSkillsDir: join4(home, ".continue/skills"),
|
|
498
699
|
detectMarker: ".continue"
|
|
499
700
|
},
|
|
500
701
|
crush: {
|
|
501
702
|
name: "crush",
|
|
502
703
|
displayName: "Crush",
|
|
503
704
|
skillsDir: ".crush/skills",
|
|
504
|
-
globalSkillsDir:
|
|
705
|
+
globalSkillsDir: join4(home, ".config/crush/skills")
|
|
505
706
|
},
|
|
506
707
|
cursor: {
|
|
507
708
|
name: "cursor",
|
|
508
709
|
displayName: "Cursor",
|
|
509
|
-
skillsDir: ".
|
|
510
|
-
globalSkillsDir:
|
|
710
|
+
skillsDir: ".agents/skills",
|
|
711
|
+
globalSkillsDir: join4(home, ".cursor/skills"),
|
|
511
712
|
detectMarker: ".cursor"
|
|
512
713
|
},
|
|
513
714
|
droid: {
|
|
514
715
|
name: "droid",
|
|
515
716
|
displayName: "Droid",
|
|
516
717
|
skillsDir: ".factory/skills",
|
|
517
|
-
globalSkillsDir:
|
|
718
|
+
globalSkillsDir: join4(home, ".factory/skills"),
|
|
518
719
|
detectMarker: ".factory"
|
|
519
720
|
},
|
|
520
721
|
"gemini-cli": {
|
|
521
722
|
name: "gemini-cli",
|
|
522
723
|
displayName: "Gemini CLI",
|
|
523
724
|
skillsDir: ".agents/skills",
|
|
524
|
-
globalSkillsDir:
|
|
725
|
+
globalSkillsDir: join4(home, ".gemini/skills")
|
|
525
726
|
},
|
|
526
727
|
"github-copilot": {
|
|
527
728
|
name: "github-copilot",
|
|
528
729
|
displayName: "GitHub Copilot",
|
|
529
730
|
skillsDir: ".agents/skills",
|
|
530
|
-
globalSkillsDir:
|
|
731
|
+
globalSkillsDir: join4(home, ".copilot/skills")
|
|
531
732
|
},
|
|
532
733
|
goose: {
|
|
533
734
|
name: "goose",
|
|
534
735
|
displayName: "Goose",
|
|
535
736
|
skillsDir: ".goose/skills",
|
|
536
|
-
globalSkillsDir:
|
|
737
|
+
globalSkillsDir: join4(configHome, "goose/skills"),
|
|
537
738
|
detectMarker: ".goose"
|
|
538
739
|
},
|
|
539
740
|
junie: {
|
|
540
741
|
name: "junie",
|
|
541
742
|
displayName: "Junie",
|
|
542
743
|
skillsDir: ".junie/skills",
|
|
543
|
-
globalSkillsDir:
|
|
744
|
+
globalSkillsDir: join4(home, ".junie/skills"),
|
|
544
745
|
detectMarker: ".junie"
|
|
545
746
|
},
|
|
546
747
|
"iflow-cli": {
|
|
547
748
|
name: "iflow-cli",
|
|
548
749
|
displayName: "iFlow CLI",
|
|
549
750
|
skillsDir: ".iflow/skills",
|
|
550
|
-
globalSkillsDir:
|
|
751
|
+
globalSkillsDir: join4(home, ".iflow/skills"),
|
|
551
752
|
detectMarker: ".iflow"
|
|
552
753
|
},
|
|
553
754
|
kilo: {
|
|
554
755
|
name: "kilo",
|
|
555
756
|
displayName: "Kilo Code",
|
|
556
757
|
skillsDir: ".kilocode/skills",
|
|
557
|
-
globalSkillsDir:
|
|
758
|
+
globalSkillsDir: join4(home, ".kilocode/skills"),
|
|
558
759
|
detectMarker: ".kilocode"
|
|
559
760
|
},
|
|
560
761
|
"kimi-cli": {
|
|
561
762
|
name: "kimi-cli",
|
|
562
763
|
displayName: "Kimi Code CLI",
|
|
563
764
|
skillsDir: ".agents/skills",
|
|
564
|
-
globalSkillsDir:
|
|
765
|
+
globalSkillsDir: join4(home, ".config/agents/skills")
|
|
565
766
|
},
|
|
566
767
|
"kiro-cli": {
|
|
567
768
|
name: "kiro-cli",
|
|
568
769
|
displayName: "Kiro CLI",
|
|
569
770
|
skillsDir: ".kiro/skills",
|
|
570
|
-
globalSkillsDir:
|
|
771
|
+
globalSkillsDir: join4(home, ".kiro/skills"),
|
|
571
772
|
detectMarker: ".kiro"
|
|
572
773
|
},
|
|
573
774
|
kode: {
|
|
574
775
|
name: "kode",
|
|
575
776
|
displayName: "Kode",
|
|
576
777
|
skillsDir: ".kode/skills",
|
|
577
|
-
globalSkillsDir:
|
|
778
|
+
globalSkillsDir: join4(home, ".kode/skills"),
|
|
578
779
|
detectMarker: ".kode"
|
|
579
780
|
},
|
|
580
781
|
mcpjam: {
|
|
581
782
|
name: "mcpjam",
|
|
582
783
|
displayName: "MCPJam",
|
|
583
784
|
skillsDir: ".mcpjam/skills",
|
|
584
|
-
globalSkillsDir:
|
|
785
|
+
globalSkillsDir: join4(home, ".mcpjam/skills"),
|
|
585
786
|
detectMarker: ".mcpjam"
|
|
586
787
|
},
|
|
587
788
|
"mistral-vibe": {
|
|
588
789
|
name: "mistral-vibe",
|
|
589
790
|
displayName: "Mistral Vibe",
|
|
590
791
|
skillsDir: ".vibe/skills",
|
|
591
|
-
globalSkillsDir:
|
|
792
|
+
globalSkillsDir: join4(home, ".vibe/skills"),
|
|
592
793
|
detectMarker: ".vibe"
|
|
593
794
|
},
|
|
594
795
|
mux: {
|
|
595
796
|
name: "mux",
|
|
596
797
|
displayName: "Mux",
|
|
597
798
|
skillsDir: ".mux/skills",
|
|
598
|
-
globalSkillsDir:
|
|
799
|
+
globalSkillsDir: join4(home, ".mux/skills"),
|
|
599
800
|
detectMarker: ".mux"
|
|
600
801
|
},
|
|
601
802
|
opencode: {
|
|
602
803
|
name: "opencode",
|
|
603
804
|
displayName: "OpenCode",
|
|
604
|
-
skillsDir: ".
|
|
605
|
-
globalSkillsDir:
|
|
805
|
+
skillsDir: ".agents/skills",
|
|
806
|
+
globalSkillsDir: join4(configHome, "opencode/skills")
|
|
606
807
|
},
|
|
607
808
|
openhands: {
|
|
608
809
|
name: "openhands",
|
|
609
810
|
displayName: "OpenHands",
|
|
610
811
|
skillsDir: ".openhands/skills",
|
|
611
|
-
globalSkillsDir:
|
|
812
|
+
globalSkillsDir: join4(home, ".openhands/skills"),
|
|
612
813
|
detectMarker: ".openhands"
|
|
613
814
|
},
|
|
614
815
|
pi: {
|
|
615
816
|
name: "pi",
|
|
616
817
|
displayName: "Pi",
|
|
617
818
|
skillsDir: ".pi/skills",
|
|
618
|
-
globalSkillsDir:
|
|
819
|
+
globalSkillsDir: join4(home, ".pi/agent/skills"),
|
|
619
820
|
detectMarker: ".pi"
|
|
620
821
|
},
|
|
621
822
|
qoder: {
|
|
622
823
|
name: "qoder",
|
|
623
824
|
displayName: "Qoder",
|
|
624
825
|
skillsDir: ".qoder/skills",
|
|
625
|
-
globalSkillsDir:
|
|
826
|
+
globalSkillsDir: join4(home, ".qoder/skills"),
|
|
626
827
|
detectMarker: ".qoder"
|
|
627
828
|
},
|
|
628
829
|
"qwen-code": {
|
|
629
830
|
name: "qwen-code",
|
|
630
831
|
displayName: "Qwen Code",
|
|
631
832
|
skillsDir: ".qwen/skills",
|
|
632
|
-
globalSkillsDir:
|
|
833
|
+
globalSkillsDir: join4(home, ".qwen/skills"),
|
|
633
834
|
detectMarker: ".qwen"
|
|
634
835
|
},
|
|
635
836
|
replit: {
|
|
636
837
|
name: "replit",
|
|
637
838
|
displayName: "Replit",
|
|
638
839
|
skillsDir: ".agents/skills",
|
|
639
|
-
globalSkillsDir:
|
|
840
|
+
globalSkillsDir: join4(configHome, "agents/skills"),
|
|
841
|
+
showInUniversalList: false
|
|
640
842
|
},
|
|
641
843
|
roo: {
|
|
642
844
|
name: "roo",
|
|
643
845
|
displayName: "Roo Code",
|
|
644
846
|
skillsDir: ".roo/skills",
|
|
645
|
-
globalSkillsDir:
|
|
847
|
+
globalSkillsDir: join4(home, ".roo/skills"),
|
|
646
848
|
detectMarker: ".roo"
|
|
647
849
|
},
|
|
648
850
|
trae: {
|
|
649
851
|
name: "trae",
|
|
650
852
|
displayName: "Trae",
|
|
651
853
|
skillsDir: ".trae/skills",
|
|
652
|
-
globalSkillsDir:
|
|
854
|
+
globalSkillsDir: join4(home, ".trae/skills"),
|
|
653
855
|
detectMarker: ".trae"
|
|
654
856
|
},
|
|
655
857
|
"trae-cn": {
|
|
656
858
|
name: "trae-cn",
|
|
657
859
|
displayName: "Trae CN",
|
|
658
860
|
skillsDir: ".trae/skills",
|
|
659
|
-
globalSkillsDir:
|
|
861
|
+
globalSkillsDir: join4(home, ".trae-cn/skills")
|
|
660
862
|
},
|
|
661
863
|
windsurf: {
|
|
662
864
|
name: "windsurf",
|
|
663
865
|
displayName: "Windsurf",
|
|
664
866
|
skillsDir: ".windsurf/skills",
|
|
665
|
-
globalSkillsDir:
|
|
867
|
+
globalSkillsDir: join4(home, ".codeium/windsurf/skills"),
|
|
666
868
|
detectMarker: ".windsurf"
|
|
667
869
|
},
|
|
668
870
|
zencoder: {
|
|
669
871
|
name: "zencoder",
|
|
670
872
|
displayName: "Zencoder",
|
|
671
873
|
skillsDir: ".zencoder/skills",
|
|
672
|
-
globalSkillsDir:
|
|
874
|
+
globalSkillsDir: join4(home, ".zencoder/skills"),
|
|
673
875
|
detectMarker: ".zencoder"
|
|
674
876
|
},
|
|
675
877
|
neovate: {
|
|
676
878
|
name: "neovate",
|
|
677
879
|
displayName: "Neovate",
|
|
678
880
|
skillsDir: ".neovate/skills",
|
|
679
|
-
globalSkillsDir:
|
|
881
|
+
globalSkillsDir: join4(home, ".neovate/skills"),
|
|
680
882
|
detectMarker: ".neovate"
|
|
681
883
|
},
|
|
682
884
|
pochi: {
|
|
683
885
|
name: "pochi",
|
|
684
886
|
displayName: "Pochi",
|
|
685
887
|
skillsDir: ".pochi/skills",
|
|
686
|
-
globalSkillsDir:
|
|
888
|
+
globalSkillsDir: join4(home, ".pochi/skills"),
|
|
687
889
|
detectMarker: ".pochi"
|
|
688
890
|
},
|
|
689
891
|
adal: {
|
|
690
892
|
name: "adal",
|
|
691
893
|
displayName: "AdaL",
|
|
692
894
|
skillsDir: ".adal/skills",
|
|
693
|
-
globalSkillsDir:
|
|
895
|
+
globalSkillsDir: join4(home, ".adal/skills"),
|
|
694
896
|
detectMarker: ".adal"
|
|
897
|
+
},
|
|
898
|
+
universal: {
|
|
899
|
+
name: "universal",
|
|
900
|
+
displayName: "Universal",
|
|
901
|
+
skillsDir: ".agents/skills",
|
|
902
|
+
globalSkillsDir: join4(configHome, "agents/skills"),
|
|
903
|
+
showInUniversalList: false
|
|
695
904
|
}
|
|
696
905
|
};
|
|
697
906
|
function detectPlatform(cwd) {
|
|
698
907
|
for (const [key, config] of Object.entries(AGENTS)) {
|
|
699
|
-
if (config.detectMarker && existsSync4(
|
|
908
|
+
if (config.detectMarker && existsSync4(join4(cwd, config.detectMarker))) {
|
|
700
909
|
return key;
|
|
701
910
|
}
|
|
702
911
|
}
|
|
703
|
-
if (existsSync4(
|
|
912
|
+
if (existsSync4(join4(configHome, "opencode"))) {
|
|
704
913
|
return "opencode";
|
|
705
914
|
}
|
|
706
915
|
return "claude-code";
|
|
707
916
|
}
|
|
708
917
|
function getAgentSkillPath(cwd, agent, name) {
|
|
709
|
-
return
|
|
918
|
+
return join4(cwd, AGENTS[agent].skillsDir, name);
|
|
710
919
|
}
|
|
711
920
|
function getAgentGlobalSkillPath(agent, name) {
|
|
712
|
-
return
|
|
921
|
+
return join4(AGENTS[agent].globalSkillsDir, name);
|
|
922
|
+
}
|
|
923
|
+
function isUniversalAgent(type) {
|
|
924
|
+
return AGENTS[type].skillsDir === ".agents/skills";
|
|
713
925
|
}
|
|
714
926
|
|
|
715
927
|
// src/utils/paths.ts
|
|
716
|
-
var AGENTS_HOME =
|
|
717
|
-
var CONFIG_DIR =
|
|
718
|
-
var SKILLS_DIR =
|
|
719
|
-
var REGISTRY_PATH =
|
|
928
|
+
var AGENTS_HOME = join5(homedir2(), ".agents");
|
|
929
|
+
var CONFIG_DIR = join5(AGENTS_HOME, "config");
|
|
930
|
+
var SKILLS_DIR = join5(AGENTS_HOME, "skills");
|
|
931
|
+
var REGISTRY_PATH = join5(AGENTS_HOME, "registry.json");
|
|
720
932
|
function getSkillCanonicalPath(name) {
|
|
721
|
-
return
|
|
933
|
+
return join5(SKILLS_DIR, name);
|
|
722
934
|
}
|
|
723
935
|
function getSkillConfigPath(name) {
|
|
724
|
-
return
|
|
936
|
+
return join5(CONFIG_DIR, name);
|
|
725
937
|
}
|
|
726
938
|
|
|
727
939
|
// src/core/env-manager.ts
|
|
@@ -746,9 +958,9 @@ function serializeEnv(data) {
|
|
|
746
958
|
}
|
|
747
959
|
async function backupEnv(skillName, agentSkillDir) {
|
|
748
960
|
const locations = [
|
|
749
|
-
|
|
750
|
-
...agentSkillDir ? [
|
|
751
|
-
|
|
961
|
+
join6(getSkillConfigPath(skillName), ".env"),
|
|
962
|
+
...agentSkillDir ? [join6(agentSkillDir, ".env")] : [],
|
|
963
|
+
join6(getSkillCanonicalPath(skillName), ".env")
|
|
752
964
|
];
|
|
753
965
|
for (const loc of locations) {
|
|
754
966
|
const content = await readTextSafe(loc);
|
|
@@ -763,16 +975,16 @@ async function backupEnv(skillName, agentSkillDir) {
|
|
|
763
975
|
return null;
|
|
764
976
|
}
|
|
765
977
|
async function restoreEnv(skillName, envData, skillDir) {
|
|
766
|
-
const exampleContent = await readTextSafe(
|
|
978
|
+
const exampleContent = await readTextSafe(join6(skillDir, ".env.example"));
|
|
767
979
|
let finalContent;
|
|
768
980
|
if (exampleContent) {
|
|
769
981
|
finalContent = mergeEnv(envData, exampleContent);
|
|
770
982
|
} else {
|
|
771
983
|
finalContent = serializeEnv(envData);
|
|
772
984
|
}
|
|
773
|
-
const configEnvPath =
|
|
985
|
+
const configEnvPath = join6(getSkillConfigPath(skillName), ".env");
|
|
774
986
|
await writeText(configEnvPath, finalContent);
|
|
775
|
-
const skillEnvPath =
|
|
987
|
+
const skillEnvPath = join6(skillDir, ".env");
|
|
776
988
|
await writeText(skillEnvPath, finalContent);
|
|
777
989
|
debug(`Restored .env to ${configEnvPath} and ${skillEnvPath}`);
|
|
778
990
|
}
|
|
@@ -796,7 +1008,7 @@ function mergeEnv(existing, exampleContent) {
|
|
|
796
1008
|
}
|
|
797
1009
|
async function getEnvStatus(skillName, requiredKeys) {
|
|
798
1010
|
if (requiredKeys.length === 0) return "configured";
|
|
799
|
-
const configEnvPath =
|
|
1011
|
+
const configEnvPath = join6(getSkillConfigPath(skillName), ".env");
|
|
800
1012
|
const content = await readTextSafe(configEnvPath);
|
|
801
1013
|
if (!content) return "missing";
|
|
802
1014
|
const data = parseEnvFile(content);
|
|
@@ -808,18 +1020,18 @@ async function getEnvStatus(skillName, requiredKeys) {
|
|
|
808
1020
|
return "missing";
|
|
809
1021
|
}
|
|
810
1022
|
async function setEnvValue(skillName, key, value, skillDir) {
|
|
811
|
-
const configEnvPath =
|
|
1023
|
+
const configEnvPath = join6(getSkillConfigPath(skillName), ".env");
|
|
812
1024
|
const content = await readTextSafe(configEnvPath);
|
|
813
1025
|
const data = content ? parseEnvFile(content) : {};
|
|
814
1026
|
data[key] = value;
|
|
815
1027
|
const newContent = serializeEnv(data);
|
|
816
1028
|
await writeText(configEnvPath, newContent);
|
|
817
1029
|
if (skillDir) {
|
|
818
|
-
await writeText(
|
|
1030
|
+
await writeText(join6(skillDir, ".env"), newContent);
|
|
819
1031
|
}
|
|
820
1032
|
}
|
|
821
1033
|
function getEnvEditPath(skillName) {
|
|
822
|
-
return
|
|
1034
|
+
return join6(getSkillConfigPath(skillName), ".env");
|
|
823
1035
|
}
|
|
824
1036
|
|
|
825
1037
|
// src/core/registry.ts
|
|
@@ -924,12 +1136,22 @@ async function getRegistryEntry(skillName) {
|
|
|
924
1136
|
|
|
925
1137
|
// src/core/installer.ts
|
|
926
1138
|
var TOTAL_STEPS = 9;
|
|
1139
|
+
function sanitizeName(name) {
|
|
1140
|
+
let sanitized = name.replace(/\.\./g, "").replace(/[^a-zA-Z0-9_.-]/g, "");
|
|
1141
|
+
sanitized = sanitized.replace(/^\.+/, "");
|
|
1142
|
+
return sanitized;
|
|
1143
|
+
}
|
|
1144
|
+
function isPathSafe(targetPath, baseDir) {
|
|
1145
|
+
const rel = relative(resolve3(baseDir), resolve3(targetPath));
|
|
1146
|
+
if (!rel || rel === ".") return false;
|
|
1147
|
+
return !rel.startsWith("..") && !resolve3(rel).includes("..");
|
|
1148
|
+
}
|
|
927
1149
|
async function installSkill(options) {
|
|
928
1150
|
const { source, cwd, copy = false, force = false, global: isGlobal = false } = options;
|
|
929
1151
|
step(1, TOTAL_STEPS, "Fetching skill source...");
|
|
930
1152
|
let sourceDir;
|
|
931
1153
|
if (source.type === "git") {
|
|
932
|
-
sourceDir = await cloneRepo(source.url, source.branch);
|
|
1154
|
+
sourceDir = source.localPath ?? await cloneRepo(source.url, source.branch);
|
|
933
1155
|
} else if (source.type === "local") {
|
|
934
1156
|
sourceDir = source.path;
|
|
935
1157
|
if (!existsSync6(sourceDir)) {
|
|
@@ -948,7 +1170,10 @@ async function installSkill(options) {
|
|
|
948
1170
|
if (!parsed) {
|
|
949
1171
|
throw new SkillParseError("Failed to read SKILL.md");
|
|
950
1172
|
}
|
|
951
|
-
const skillName = parsed.frontmatter.name;
|
|
1173
|
+
const skillName = sanitizeName(parsed.frontmatter.name);
|
|
1174
|
+
if (!skillName) {
|
|
1175
|
+
throw new SkillParseError("Skill name is empty after sanitization");
|
|
1176
|
+
}
|
|
952
1177
|
info(`Found skill: ${skillName}${parsed.frontmatter.version ? ` v${parsed.frontmatter.version}` : ""}`);
|
|
953
1178
|
step(4, TOTAL_STEPS, "Detecting agent platform...");
|
|
954
1179
|
const agent = options.agent ?? detectPlatform(cwd);
|
|
@@ -963,6 +1188,9 @@ async function installSkill(options) {
|
|
|
963
1188
|
}
|
|
964
1189
|
step(6, TOTAL_STEPS, "Installing to canonical path...");
|
|
965
1190
|
const canonicalPath = getSkillCanonicalPath(skillName);
|
|
1191
|
+
if (!isPathSafe(canonicalPath, SKILLS_DIR)) {
|
|
1192
|
+
throw new SkillParseError(`Unsafe canonical path: ${canonicalPath}`);
|
|
1193
|
+
}
|
|
966
1194
|
if (existsSync6(canonicalPath) && !force) {
|
|
967
1195
|
info("Replacing existing installation");
|
|
968
1196
|
}
|
|
@@ -974,18 +1202,25 @@ async function installSkill(options) {
|
|
|
974
1202
|
await restoreEnv(skillName, envBackup, canonicalPath);
|
|
975
1203
|
success(".env restored successfully");
|
|
976
1204
|
} else {
|
|
977
|
-
const examplePath =
|
|
1205
|
+
const examplePath = join7(canonicalPath, ".env.example");
|
|
978
1206
|
if (existsSync6(examplePath)) {
|
|
979
1207
|
warn("Found .env.example \u2014 run `skill-master env edit " + skillName + "` to configure");
|
|
980
1208
|
}
|
|
981
1209
|
}
|
|
982
1210
|
step(8, TOTAL_STEPS, `Linking to ${agent} skills directory...`);
|
|
983
1211
|
const agentPath = isGlobal ? getAgentGlobalSkillPath(agent, skillName) : getAgentSkillPath(cwd, agent, skillName);
|
|
984
|
-
|
|
985
|
-
|
|
1212
|
+
let installMode;
|
|
1213
|
+
if (isGlobal && isUniversalAgent(agent) && canonicalPath === agentPath) {
|
|
1214
|
+
installMode = "copy";
|
|
1215
|
+
success(`Canonical path is agent path (universal agent): ${agentPath}`);
|
|
1216
|
+
} else {
|
|
1217
|
+
const linkType = await symlinkOrCopy(canonicalPath, agentPath, copy);
|
|
1218
|
+
installMode = linkType;
|
|
1219
|
+
success(`${linkType === "symlink" ? "Symlinked" : "Copied"} to ${agentPath}`);
|
|
1220
|
+
}
|
|
986
1221
|
step(9, TOTAL_STEPS, "Updating registry...");
|
|
987
1222
|
const capabilities = parsed.frontmatter.capabilities ?? inferCapabilities(parsed.frontmatter["allowed-tools"] ?? []);
|
|
988
|
-
const envExampleContent = await readTextSafe(
|
|
1223
|
+
const envExampleContent = await readTextSafe(join7(canonicalPath, ".env.example"));
|
|
989
1224
|
const envKeys = envExampleContent ? extractEnvKeys(envExampleContent) : [];
|
|
990
1225
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
991
1226
|
const agentInstall = {
|
|
@@ -1006,11 +1241,81 @@ async function installSkill(options) {
|
|
|
1006
1241
|
await updateRegistry(skillName, entry);
|
|
1007
1242
|
blank();
|
|
1008
1243
|
success(`Skill "${skillName}" installed successfully!`);
|
|
1244
|
+
return {
|
|
1245
|
+
skillName,
|
|
1246
|
+
version: parsed.frontmatter.version,
|
|
1247
|
+
canonicalPath,
|
|
1248
|
+
agentPath,
|
|
1249
|
+
installMode
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/core/local-lock.ts
|
|
1254
|
+
import { join as join8 } from "path";
|
|
1255
|
+
import { readdir as readdir3, readFile as readFile3 } from "fs/promises";
|
|
1256
|
+
import { createHash } from "crypto";
|
|
1257
|
+
var LOCK_FILENAME = "skills-lock.json";
|
|
1258
|
+
var HASH_EXCLUDES = /* @__PURE__ */ new Set([".env", ".git", "node_modules"]);
|
|
1259
|
+
function getLocalLockPath(cwd) {
|
|
1260
|
+
return join8(cwd, LOCK_FILENAME);
|
|
1261
|
+
}
|
|
1262
|
+
function createEmptyLock() {
|
|
1263
|
+
return { version: 1, skills: {} };
|
|
1264
|
+
}
|
|
1265
|
+
async function readLocalLock(cwd) {
|
|
1266
|
+
const lockPath = getLocalLockPath(cwd);
|
|
1267
|
+
const data = await readJsonSafe(lockPath);
|
|
1268
|
+
if (!data || typeof data !== "object" || data.version !== 1 || data.skills === null || Array.isArray(data.skills) || typeof data.skills !== "object") {
|
|
1269
|
+
return createEmptyLock();
|
|
1270
|
+
}
|
|
1271
|
+
return data;
|
|
1272
|
+
}
|
|
1273
|
+
async function writeLocalLock(lock, cwd) {
|
|
1274
|
+
const lockPath = getLocalLockPath(cwd);
|
|
1275
|
+
const sorted = {};
|
|
1276
|
+
for (const key of Object.keys(lock.skills).sort()) {
|
|
1277
|
+
sorted[key] = lock.skills[key];
|
|
1278
|
+
}
|
|
1279
|
+
await atomicWriteJson(lockPath, { version: lock.version, skills: sorted });
|
|
1280
|
+
}
|
|
1281
|
+
async function computeSkillFolderHash(dirPath) {
|
|
1282
|
+
const hash = createHash("sha256");
|
|
1283
|
+
await hashDir(dirPath, "", hash);
|
|
1284
|
+
return hash.digest("hex");
|
|
1285
|
+
}
|
|
1286
|
+
async function hashDir(basePath, relativePath, hash) {
|
|
1287
|
+
const fullPath = relativePath ? join8(basePath, relativePath) : basePath;
|
|
1288
|
+
const entries = await readdir3(fullPath, { withFileTypes: true });
|
|
1289
|
+
const sorted = entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1290
|
+
for (const entry of sorted) {
|
|
1291
|
+
if (HASH_EXCLUDES.has(entry.name)) continue;
|
|
1292
|
+
const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1293
|
+
if (entry.isDirectory()) {
|
|
1294
|
+
await hashDir(basePath, entryRelative, hash);
|
|
1295
|
+
} else if (entry.isFile()) {
|
|
1296
|
+
hash.update(entryRelative);
|
|
1297
|
+
hash.update("\0");
|
|
1298
|
+
const content = await readFile3(join8(basePath, entryRelative));
|
|
1299
|
+
hash.update(content);
|
|
1300
|
+
hash.update("\0");
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
1305
|
+
const lock = await readLocalLock(cwd);
|
|
1306
|
+
lock.skills[skillName] = entry;
|
|
1307
|
+
await writeLocalLock(lock, cwd);
|
|
1308
|
+
}
|
|
1309
|
+
async function removeSkillFromLocalLock(skillName, cwd) {
|
|
1310
|
+
const lock = await readLocalLock(cwd);
|
|
1311
|
+
if (!(skillName in lock.skills)) return;
|
|
1312
|
+
delete lock.skills[skillName];
|
|
1313
|
+
await writeLocalLock(lock, cwd);
|
|
1009
1314
|
}
|
|
1010
1315
|
|
|
1011
1316
|
// src/commands/add.ts
|
|
1012
1317
|
import { existsSync as existsSync7 } from "fs";
|
|
1013
|
-
import { basename as basename2, join as
|
|
1318
|
+
import { basename as basename2, join as join9, relative as relative2 } from "path";
|
|
1014
1319
|
function parseAddFlags(args) {
|
|
1015
1320
|
const flags = {
|
|
1016
1321
|
global: false,
|
|
@@ -1154,7 +1459,7 @@ async function add(args) {
|
|
|
1154
1459
|
step(1, 9, "Fetching skill source...");
|
|
1155
1460
|
sourceDir = await cloneRepo(parsed.url, parsed.ref);
|
|
1156
1461
|
if (parsed.subpath) {
|
|
1157
|
-
const sub =
|
|
1462
|
+
const sub = join9(sourceDir, parsed.subpath);
|
|
1158
1463
|
if (existsSync7(sub)) {
|
|
1159
1464
|
sourceDir = sub;
|
|
1160
1465
|
}
|
|
@@ -1165,14 +1470,14 @@ async function add(args) {
|
|
|
1165
1470
|
throw new SkillNotFoundError(sourceDir);
|
|
1166
1471
|
}
|
|
1167
1472
|
}
|
|
1168
|
-
const allSkillDirs = await
|
|
1473
|
+
const allSkillDirs = await findAllSkillDirectoriesWithPlugins(sourceDir, flags.fullDepth);
|
|
1169
1474
|
if (allSkillDirs.length === 0) {
|
|
1170
1475
|
throw new SkillNotFoundError(`No SKILL.md found in ${sourceDir}`);
|
|
1171
1476
|
}
|
|
1172
1477
|
if (flags.list) {
|
|
1173
1478
|
blank();
|
|
1174
1479
|
tableHeader("Skill", "Version", "Description");
|
|
1175
|
-
for (const dir of allSkillDirs) {
|
|
1480
|
+
for (const { path: dir } of allSkillDirs) {
|
|
1176
1481
|
const sk = await readSkillMd(dir);
|
|
1177
1482
|
if (sk) {
|
|
1178
1483
|
tableRow(
|
|
@@ -1189,18 +1494,18 @@ async function add(args) {
|
|
|
1189
1494
|
if (flags.skill.length > 0 && !flags.skill.includes("*")) {
|
|
1190
1495
|
const requested = new Set(flags.skill.map((s) => s.toLowerCase()));
|
|
1191
1496
|
const filtered = [];
|
|
1192
|
-
for (const
|
|
1193
|
-
const sk = await readSkillMd(
|
|
1497
|
+
for (const item of allSkillDirs) {
|
|
1498
|
+
const sk = await readSkillMd(item.path);
|
|
1194
1499
|
if (!sk) continue;
|
|
1195
1500
|
const name = sk.frontmatter.name.toLowerCase();
|
|
1196
|
-
const dirName = basename2(
|
|
1501
|
+
const dirName = basename2(item.path).toLowerCase();
|
|
1197
1502
|
if (requested.has(name) || requested.has(dirName)) {
|
|
1198
|
-
filtered.push(
|
|
1503
|
+
filtered.push(item);
|
|
1199
1504
|
}
|
|
1200
1505
|
}
|
|
1201
1506
|
if (filtered.length === 0) {
|
|
1202
1507
|
const available = [];
|
|
1203
|
-
for (const dir of allSkillDirs) {
|
|
1508
|
+
for (const { path: dir } of allSkillDirs) {
|
|
1204
1509
|
const sk = await readSkillMd(dir);
|
|
1205
1510
|
if (sk) available.push(sk.frontmatter.name);
|
|
1206
1511
|
}
|
|
@@ -1214,10 +1519,11 @@ async function add(args) {
|
|
|
1214
1519
|
}
|
|
1215
1520
|
const agents = flags.agent.length > 0 ? flags.agent : [void 0];
|
|
1216
1521
|
try {
|
|
1217
|
-
for (const dir of targetDirs) {
|
|
1522
|
+
for (const { path: dir, pluginName } of targetDirs) {
|
|
1218
1523
|
for (const agent of agents) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1524
|
+
const installSource = parsed.type === "git" ? { type: "git", url: parsed.url, branch: parsed.ref, localPath: dir } : { type: "local", path: dir };
|
|
1525
|
+
const result = await installSkill({
|
|
1526
|
+
source: installSource,
|
|
1221
1527
|
agent,
|
|
1222
1528
|
cwd,
|
|
1223
1529
|
global: flags.global,
|
|
@@ -1225,6 +1531,16 @@ async function add(args) {
|
|
|
1225
1531
|
force: flags.force,
|
|
1226
1532
|
yes: flags.yes
|
|
1227
1533
|
});
|
|
1534
|
+
if (!flags.global) {
|
|
1535
|
+
const skillDir = relative2(sourceDir, dir);
|
|
1536
|
+
await addSkillToLocalLock(result.skillName, {
|
|
1537
|
+
source,
|
|
1538
|
+
sourceType: parsed.type === "git" ? "github" : "local",
|
|
1539
|
+
computedHash: await computeSkillFolderHash(result.canonicalPath),
|
|
1540
|
+
...skillDir && skillDir !== "." ? { skillDir } : {},
|
|
1541
|
+
...pluginName ? { pluginName } : {}
|
|
1542
|
+
}, cwd);
|
|
1543
|
+
}
|
|
1228
1544
|
}
|
|
1229
1545
|
}
|
|
1230
1546
|
} catch (err) {
|
|
@@ -1413,6 +1729,7 @@ async function remove(args) {
|
|
|
1413
1729
|
await removePath(getSkillConfigPath(skillName));
|
|
1414
1730
|
success("Purged config directory");
|
|
1415
1731
|
}
|
|
1732
|
+
await removeSkillFromLocalLock(skillName, process.cwd());
|
|
1416
1733
|
}
|
|
1417
1734
|
} else {
|
|
1418
1735
|
for (const agentRecord of entry.agents) {
|
|
@@ -1426,6 +1743,7 @@ async function remove(args) {
|
|
|
1426
1743
|
success("Purged config directory");
|
|
1427
1744
|
}
|
|
1428
1745
|
await removeFromRegistry(skillName);
|
|
1746
|
+
await removeSkillFromLocalLock(skillName, process.cwd());
|
|
1429
1747
|
}
|
|
1430
1748
|
success(`Skill "${skillName}" removed successfully!`);
|
|
1431
1749
|
}
|
|
@@ -1547,6 +1865,9 @@ function parseListFlags(args) {
|
|
|
1547
1865
|
}
|
|
1548
1866
|
return flags;
|
|
1549
1867
|
}
|
|
1868
|
+
function toTitleCase(str) {
|
|
1869
|
+
return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
1870
|
+
}
|
|
1550
1871
|
async function list(args = []) {
|
|
1551
1872
|
const flags = parseListFlags(args);
|
|
1552
1873
|
const skills = await listRegistry();
|
|
@@ -1560,14 +1881,55 @@ async function list(args = []) {
|
|
|
1560
1881
|
info("No skills installed");
|
|
1561
1882
|
return;
|
|
1562
1883
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1884
|
+
const cwd = process.cwd();
|
|
1885
|
+
const localLock = await readLocalLock(cwd);
|
|
1886
|
+
const groupedSkills = {};
|
|
1887
|
+
const ungroupedSkills = [];
|
|
1565
1888
|
for (const [name, entry] of entries) {
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1889
|
+
const lockEntry = localLock.skills[name];
|
|
1890
|
+
if (lockEntry?.pluginName) {
|
|
1891
|
+
const group = lockEntry.pluginName;
|
|
1892
|
+
if (!groupedSkills[group]) {
|
|
1893
|
+
groupedSkills[group] = [];
|
|
1894
|
+
}
|
|
1895
|
+
groupedSkills[group].push([name, entry]);
|
|
1896
|
+
} else {
|
|
1897
|
+
ungroupedSkills.push([name, entry]);
|
|
1898
|
+
}
|
|
1569
1899
|
}
|
|
1900
|
+
const hasGroups = Object.keys(groupedSkills).length > 0;
|
|
1570
1901
|
blank();
|
|
1902
|
+
if (hasGroups) {
|
|
1903
|
+
const sortedGroups = Object.keys(groupedSkills).sort();
|
|
1904
|
+
for (const group of sortedGroups) {
|
|
1905
|
+
section(toTitleCase(group));
|
|
1906
|
+
tableHeader("Skill", "Version", "Platform(s)", "Installed");
|
|
1907
|
+
for (const [name, entry] of groupedSkills[group]) {
|
|
1908
|
+
const date = new Date(entry.installed_at).toLocaleDateString();
|
|
1909
|
+
const platforms = entry.agents.map((a) => a.agent).join(", ");
|
|
1910
|
+
tableRow(name, entry.version ?? "-", platforms, date);
|
|
1911
|
+
}
|
|
1912
|
+
blank();
|
|
1913
|
+
}
|
|
1914
|
+
if (ungroupedSkills.length > 0) {
|
|
1915
|
+
section("General");
|
|
1916
|
+
tableHeader("Skill", "Version", "Platform(s)", "Installed");
|
|
1917
|
+
for (const [name, entry] of ungroupedSkills) {
|
|
1918
|
+
const date = new Date(entry.installed_at).toLocaleDateString();
|
|
1919
|
+
const platforms = entry.agents.map((a) => a.agent).join(", ");
|
|
1920
|
+
tableRow(name, entry.version ?? "-", platforms, date);
|
|
1921
|
+
}
|
|
1922
|
+
blank();
|
|
1923
|
+
}
|
|
1924
|
+
} else {
|
|
1925
|
+
tableHeader("Skill", "Version", "Platform(s)", "Installed");
|
|
1926
|
+
for (const [name, entry] of entries) {
|
|
1927
|
+
const date = new Date(entry.installed_at).toLocaleDateString();
|
|
1928
|
+
const platforms = entry.agents.map((a) => a.agent).join(", ");
|
|
1929
|
+
tableRow(name, entry.version ?? "-", platforms, date);
|
|
1930
|
+
}
|
|
1931
|
+
blank();
|
|
1932
|
+
}
|
|
1571
1933
|
}
|
|
1572
1934
|
|
|
1573
1935
|
// src/commands/info.ts
|
|
@@ -1703,13 +2065,14 @@ async function find(args) {
|
|
|
1703
2065
|
process.exit(1);
|
|
1704
2066
|
}
|
|
1705
2067
|
const data = await response.json();
|
|
1706
|
-
|
|
2068
|
+
const skills = data.skills ?? [];
|
|
2069
|
+
if (skills.length === 0) {
|
|
1707
2070
|
info("No skills found matching your query.");
|
|
1708
2071
|
return;
|
|
1709
2072
|
}
|
|
1710
2073
|
blank();
|
|
1711
2074
|
tableHeader("Name", "Source", "Installs");
|
|
1712
|
-
for (const item of
|
|
2075
|
+
for (const item of skills) {
|
|
1713
2076
|
tableRow(
|
|
1714
2077
|
item.name ?? "\u2014",
|
|
1715
2078
|
item.source ?? "\u2014",
|
|
@@ -1729,7 +2092,7 @@ async function find(args) {
|
|
|
1729
2092
|
|
|
1730
2093
|
// src/commands/init.ts
|
|
1731
2094
|
import { existsSync as existsSync9 } from "fs";
|
|
1732
|
-
import { join as
|
|
2095
|
+
import { join as join10, basename as basename3 } from "path";
|
|
1733
2096
|
var SKILL_MD_TEMPLATE = `---
|
|
1734
2097
|
name: {{NAME}}
|
|
1735
2098
|
version: 0.1.0
|
|
@@ -1755,13 +2118,13 @@ async function init(args) {
|
|
|
1755
2118
|
let targetDir;
|
|
1756
2119
|
let skillName;
|
|
1757
2120
|
if (nameArg) {
|
|
1758
|
-
targetDir =
|
|
2121
|
+
targetDir = join10(cwd, nameArg);
|
|
1759
2122
|
skillName = nameArg;
|
|
1760
2123
|
} else {
|
|
1761
2124
|
targetDir = cwd;
|
|
1762
2125
|
skillName = basename3(cwd);
|
|
1763
2126
|
}
|
|
1764
|
-
const skillMdPath =
|
|
2127
|
+
const skillMdPath = join10(targetDir, "SKILL.md");
|
|
1765
2128
|
if (existsSync9(skillMdPath)) {
|
|
1766
2129
|
error(`SKILL.md already exists at ${skillMdPath}`);
|
|
1767
2130
|
process.exit(1);
|
|
@@ -1834,6 +2197,301 @@ async function getRemoteHead(source) {
|
|
|
1834
2197
|
}
|
|
1835
2198
|
}
|
|
1836
2199
|
|
|
2200
|
+
// src/commands/sync.ts
|
|
2201
|
+
import { relative as relative3 } from "path";
|
|
2202
|
+
function parseSyncFlags(args) {
|
|
2203
|
+
const flags = {
|
|
2204
|
+
agent: [],
|
|
2205
|
+
yes: false,
|
|
2206
|
+
force: false,
|
|
2207
|
+
help: false
|
|
2208
|
+
};
|
|
2209
|
+
let i = 0;
|
|
2210
|
+
while (i < args.length) {
|
|
2211
|
+
const arg = args[i];
|
|
2212
|
+
switch (arg) {
|
|
2213
|
+
case "-h":
|
|
2214
|
+
case "--help":
|
|
2215
|
+
flags.help = true;
|
|
2216
|
+
i++;
|
|
2217
|
+
break;
|
|
2218
|
+
case "-a":
|
|
2219
|
+
case "--agent":
|
|
2220
|
+
i++;
|
|
2221
|
+
while (i < args.length && !args[i].startsWith("-")) {
|
|
2222
|
+
flags.agent.push(args[i]);
|
|
2223
|
+
i++;
|
|
2224
|
+
}
|
|
2225
|
+
break;
|
|
2226
|
+
case "-y":
|
|
2227
|
+
case "--yes":
|
|
2228
|
+
flags.yes = true;
|
|
2229
|
+
i++;
|
|
2230
|
+
break;
|
|
2231
|
+
case "-f":
|
|
2232
|
+
case "--force":
|
|
2233
|
+
flags.force = true;
|
|
2234
|
+
i++;
|
|
2235
|
+
break;
|
|
2236
|
+
default:
|
|
2237
|
+
if (arg.startsWith("-")) {
|
|
2238
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
2239
|
+
}
|
|
2240
|
+
i++;
|
|
2241
|
+
break;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return { flags };
|
|
2245
|
+
}
|
|
2246
|
+
function printSyncHelp() {
|
|
2247
|
+
console.log("Usage: skill-master sync [options]");
|
|
2248
|
+
console.log("");
|
|
2249
|
+
console.log("Discover and sync skills from node_modules.");
|
|
2250
|
+
console.log("");
|
|
2251
|
+
console.log("Options:");
|
|
2252
|
+
console.log(" -h, --help Show this help message");
|
|
2253
|
+
console.log(" -a, --agent <agents> Target agents (space-separated)");
|
|
2254
|
+
console.log(" -y, --yes Skip confirmations");
|
|
2255
|
+
console.log(" -f, --force Force reinstall even if unchanged");
|
|
2256
|
+
}
|
|
2257
|
+
async function sync(args) {
|
|
2258
|
+
const { flags } = parseSyncFlags(args);
|
|
2259
|
+
if (flags.help) {
|
|
2260
|
+
printSyncHelp();
|
|
2261
|
+
process.exit(0);
|
|
2262
|
+
}
|
|
2263
|
+
const cwd = process.cwd();
|
|
2264
|
+
info("Scanning node_modules for skills...");
|
|
2265
|
+
const skillDirs = await discoverNodeModulesSkills(cwd);
|
|
2266
|
+
if (skillDirs.length === 0) {
|
|
2267
|
+
info("No skills found in node_modules.");
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
const lock = await readLocalLock(cwd);
|
|
2271
|
+
const skills = [];
|
|
2272
|
+
for (const dir of skillDirs) {
|
|
2273
|
+
const parsed = await readSkillMd(dir);
|
|
2274
|
+
if (!parsed) continue;
|
|
2275
|
+
const name = sanitizeName(parsed.frontmatter.name);
|
|
2276
|
+
const currentHash = await computeSkillFolderHash(dir);
|
|
2277
|
+
const lockEntry = lock.skills[name];
|
|
2278
|
+
let status;
|
|
2279
|
+
if (!lockEntry) {
|
|
2280
|
+
status = "new";
|
|
2281
|
+
} else if (lockEntry.computedHash !== currentHash || flags.force) {
|
|
2282
|
+
status = "updated";
|
|
2283
|
+
} else {
|
|
2284
|
+
status = "unchanged";
|
|
2285
|
+
}
|
|
2286
|
+
skills.push({ dir, name, version: parsed.frontmatter.version, status });
|
|
2287
|
+
}
|
|
2288
|
+
const actionable = skills.filter((s) => s.status !== "unchanged");
|
|
2289
|
+
if (actionable.length === 0) {
|
|
2290
|
+
success("All node_modules skills are up to date.");
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
blank();
|
|
2294
|
+
tableHeader("Skill", "Version", "Status");
|
|
2295
|
+
for (const s of skills) {
|
|
2296
|
+
tableRow(s.name, s.version ?? "-", s.status);
|
|
2297
|
+
}
|
|
2298
|
+
blank();
|
|
2299
|
+
info(`${actionable.length} skill(s) to install/update.`);
|
|
2300
|
+
if (!flags.yes) {
|
|
2301
|
+
const readline = await import("readline");
|
|
2302
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2303
|
+
const answer = await new Promise((resolve4) => {
|
|
2304
|
+
rl.question("Proceed? [y/N] ", resolve4);
|
|
2305
|
+
});
|
|
2306
|
+
rl.close();
|
|
2307
|
+
if (answer.toLowerCase() !== "y") {
|
|
2308
|
+
info("Aborted.");
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
const agents = flags.agent.length > 0 ? flags.agent : [void 0];
|
|
2313
|
+
for (const s of actionable) {
|
|
2314
|
+
for (const agent of agents) {
|
|
2315
|
+
try {
|
|
2316
|
+
const result = await installSkill({
|
|
2317
|
+
source: { type: "local", path: s.dir },
|
|
2318
|
+
agent,
|
|
2319
|
+
cwd,
|
|
2320
|
+
global: false,
|
|
2321
|
+
copy: false,
|
|
2322
|
+
force: flags.force,
|
|
2323
|
+
yes: true
|
|
2324
|
+
});
|
|
2325
|
+
await addSkillToLocalLock(result.skillName, {
|
|
2326
|
+
source: relative3(cwd, s.dir),
|
|
2327
|
+
sourceType: "node_modules",
|
|
2328
|
+
computedHash: await computeSkillFolderHash(result.canonicalPath)
|
|
2329
|
+
}, cwd);
|
|
2330
|
+
} catch (err) {
|
|
2331
|
+
error(`Failed to install ${s.name}: ${err.message}`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
blank();
|
|
2336
|
+
success(`Synced ${actionable.length} skill(s) from node_modules.`);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// src/commands/restore.ts
|
|
2340
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2341
|
+
function parseRestoreFlags(args) {
|
|
2342
|
+
const flags = { help: false };
|
|
2343
|
+
for (const arg of args) {
|
|
2344
|
+
if (arg === "-h" || arg === "--help") {
|
|
2345
|
+
flags.help = true;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return { flags };
|
|
2349
|
+
}
|
|
2350
|
+
function printRestoreHelp() {
|
|
2351
|
+
console.log("Usage: skill-master restore [options]");
|
|
2352
|
+
console.log("");
|
|
2353
|
+
console.log("Restore skills from skills-lock.json.");
|
|
2354
|
+
console.log("");
|
|
2355
|
+
console.log("Options:");
|
|
2356
|
+
console.log(" -h, --help Show this help message");
|
|
2357
|
+
}
|
|
2358
|
+
async function restore(args) {
|
|
2359
|
+
const { flags } = parseRestoreFlags(args);
|
|
2360
|
+
if (flags.help) {
|
|
2361
|
+
printRestoreHelp();
|
|
2362
|
+
process.exit(0);
|
|
2363
|
+
}
|
|
2364
|
+
const cwd = process.cwd();
|
|
2365
|
+
const lock = await readLocalLock(cwd);
|
|
2366
|
+
const entries = Object.entries(lock.skills);
|
|
2367
|
+
if (entries.length === 0) {
|
|
2368
|
+
info("No skills found in skills-lock.json. Nothing to restore.");
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
info(`Restoring ${entries.length} skill(s) from skills-lock.json...`);
|
|
2372
|
+
blank();
|
|
2373
|
+
const github = [];
|
|
2374
|
+
const nodeModules = [];
|
|
2375
|
+
const local = [];
|
|
2376
|
+
for (const [name, entry] of entries) {
|
|
2377
|
+
switch (entry.sourceType) {
|
|
2378
|
+
case "github":
|
|
2379
|
+
github.push([name, entry]);
|
|
2380
|
+
break;
|
|
2381
|
+
case "node_modules":
|
|
2382
|
+
nodeModules.push([name, entry]);
|
|
2383
|
+
break;
|
|
2384
|
+
case "local":
|
|
2385
|
+
local.push([name, entry]);
|
|
2386
|
+
break;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
let installed = 0;
|
|
2390
|
+
let failed = 0;
|
|
2391
|
+
if (nodeModules.length > 0) {
|
|
2392
|
+
info(`Syncing ${nodeModules.length} node_modules skill(s)...`);
|
|
2393
|
+
const nmSkillDirs = await discoverNodeModulesSkills(cwd);
|
|
2394
|
+
const nmMap = /* @__PURE__ */ new Map();
|
|
2395
|
+
for (const dir of nmSkillDirs) {
|
|
2396
|
+
const parsed = await readSkillMd(dir);
|
|
2397
|
+
if (parsed) nmMap.set(sanitizeName(parsed.frontmatter.name), dir);
|
|
2398
|
+
}
|
|
2399
|
+
for (const [name] of nodeModules) {
|
|
2400
|
+
const dir = nmMap.get(name);
|
|
2401
|
+
if (!dir) {
|
|
2402
|
+
warn(`Skill "${name}" not found in node_modules \u2014 run npm install first`);
|
|
2403
|
+
failed++;
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
try {
|
|
2407
|
+
const result = await installSkill({
|
|
2408
|
+
source: { type: "local", path: dir },
|
|
2409
|
+
cwd,
|
|
2410
|
+
global: false,
|
|
2411
|
+
yes: true
|
|
2412
|
+
});
|
|
2413
|
+
await addSkillToLocalLock(result.skillName, {
|
|
2414
|
+
source: dir,
|
|
2415
|
+
sourceType: "node_modules",
|
|
2416
|
+
computedHash: await computeSkillFolderHash(result.canonicalPath)
|
|
2417
|
+
}, cwd);
|
|
2418
|
+
installed++;
|
|
2419
|
+
} catch (err) {
|
|
2420
|
+
error(`Failed to restore "${name}": ${err.message}`);
|
|
2421
|
+
failed++;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
for (const [name, entry] of github) {
|
|
2426
|
+
try {
|
|
2427
|
+
const parsed = parseSource(entry.source);
|
|
2428
|
+
if (parsed.type !== "git" || !parsed.url) {
|
|
2429
|
+
warn(`Invalid source for "${name}": ${entry.source}`);
|
|
2430
|
+
failed++;
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
const sourceDir = await cloneRepo(parsed.url, parsed.ref);
|
|
2434
|
+
let skillPath = sourceDir;
|
|
2435
|
+
if (parsed.subpath) {
|
|
2436
|
+
skillPath = `${skillPath}/${parsed.subpath}`;
|
|
2437
|
+
}
|
|
2438
|
+
if (entry.skillDir) {
|
|
2439
|
+
skillPath = `${skillPath}/${entry.skillDir}`;
|
|
2440
|
+
}
|
|
2441
|
+
const result = await installSkill({
|
|
2442
|
+
source: { type: "local", path: skillPath },
|
|
2443
|
+
cwd,
|
|
2444
|
+
global: false,
|
|
2445
|
+
yes: true
|
|
2446
|
+
});
|
|
2447
|
+
await addSkillToLocalLock(result.skillName, {
|
|
2448
|
+
source: entry.source,
|
|
2449
|
+
sourceType: "github",
|
|
2450
|
+
computedHash: await computeSkillFolderHash(result.canonicalPath),
|
|
2451
|
+
...entry.skillDir ? { skillDir: entry.skillDir } : {},
|
|
2452
|
+
...entry.pluginName ? { pluginName: entry.pluginName } : {}
|
|
2453
|
+
}, cwd);
|
|
2454
|
+
installed++;
|
|
2455
|
+
} catch (err) {
|
|
2456
|
+
error(`Failed to restore "${name}": ${err.message}`);
|
|
2457
|
+
failed++;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
for (const [name, entry] of local) {
|
|
2461
|
+
if (!existsSync10(entry.source)) {
|
|
2462
|
+
warn(`Local source not found for "${name}": ${entry.source}`);
|
|
2463
|
+
failed++;
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
try {
|
|
2467
|
+
const localPath = entry.skillDir ? `${entry.source}/${entry.skillDir}` : entry.source;
|
|
2468
|
+
const result = await installSkill({
|
|
2469
|
+
source: { type: "local", path: localPath },
|
|
2470
|
+
cwd,
|
|
2471
|
+
global: false,
|
|
2472
|
+
yes: true
|
|
2473
|
+
});
|
|
2474
|
+
await addSkillToLocalLock(result.skillName, {
|
|
2475
|
+
source: entry.source,
|
|
2476
|
+
sourceType: "local",
|
|
2477
|
+
computedHash: await computeSkillFolderHash(result.canonicalPath),
|
|
2478
|
+
...entry.skillDir ? { skillDir: entry.skillDir } : {},
|
|
2479
|
+
...entry.pluginName ? { pluginName: entry.pluginName } : {}
|
|
2480
|
+
}, cwd);
|
|
2481
|
+
installed++;
|
|
2482
|
+
} catch (err) {
|
|
2483
|
+
error(`Failed to restore "${name}": ${err.message}`);
|
|
2484
|
+
failed++;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
blank();
|
|
2488
|
+
if (failed > 0) {
|
|
2489
|
+
warn(`Restored ${installed} skill(s), ${failed} failed.`);
|
|
2490
|
+
} else {
|
|
2491
|
+
success(`Restored ${installed} skill(s) successfully.`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
1837
2495
|
// src/cli.ts
|
|
1838
2496
|
var VERSION = "0.1.0";
|
|
1839
2497
|
var HELP = `
|
|
@@ -1845,6 +2503,8 @@ Usage:
|
|
|
1845
2503
|
skill-master list [options] List installed skills (alias: ls)
|
|
1846
2504
|
skill-master find [query] Search for skills (aliases: search, f, s)
|
|
1847
2505
|
skill-master update [skill] Update skills (alias: upgrade)
|
|
2506
|
+
skill-master sync [options] Sync skills from node_modules
|
|
2507
|
+
skill-master restore Restore skills from skills-lock.json (alias: install-lock)
|
|
1848
2508
|
skill-master init [name] Create a new skill template
|
|
1849
2509
|
skill-master check Check for skill updates
|
|
1850
2510
|
skill-master env <list|set|edit> Manage environment variables
|
|
@@ -1862,12 +2522,19 @@ Add Options:
|
|
|
1862
2522
|
--copy Copy instead of symlink
|
|
1863
2523
|
--force Force reinstall
|
|
1864
2524
|
|
|
2525
|
+
Sync Options:
|
|
2526
|
+
-a, --agent <agents> Target agents (space-separated)
|
|
2527
|
+
-y, --yes Skip confirmations
|
|
2528
|
+
-f, --force Force reinstall even if unchanged
|
|
2529
|
+
|
|
1865
2530
|
Examples:
|
|
1866
2531
|
skill-master add owner/repo
|
|
1867
2532
|
skill-master add https://github.com/user/skill -a claude-code cursor -y
|
|
1868
2533
|
skill-master add ./local-skill --agent=cursor --copy
|
|
1869
2534
|
skill-master remove my-skill --purge
|
|
1870
2535
|
skill-master find "code review"
|
|
2536
|
+
skill-master sync -y
|
|
2537
|
+
skill-master restore
|
|
1871
2538
|
skill-master init my-new-skill
|
|
1872
2539
|
skill-master check
|
|
1873
2540
|
`;
|
|
@@ -1923,6 +2590,15 @@ async function main() {
|
|
|
1923
2590
|
case "check":
|
|
1924
2591
|
await check(commandArgs);
|
|
1925
2592
|
break;
|
|
2593
|
+
// sync — discover and install skills from node_modules
|
|
2594
|
+
case "sync":
|
|
2595
|
+
await sync(commandArgs);
|
|
2596
|
+
break;
|
|
2597
|
+
// restore with alias: install-lock
|
|
2598
|
+
case "restore":
|
|
2599
|
+
case "install-lock":
|
|
2600
|
+
await restore(commandArgs);
|
|
2601
|
+
break;
|
|
1926
2602
|
// env, info, doctor — skill-master extensions
|
|
1927
2603
|
case "env":
|
|
1928
2604
|
await env(commandArgs);
|