add-skill 1.0.21 → 1.0.23
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/dist/index.js +185 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -245,8 +245,9 @@ function getSkillDisplayName(skill) {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
// src/installer.ts
|
|
248
|
-
import { mkdir, cp, access, readdir as readdir2 } from "fs/promises";
|
|
249
|
-
import { join as join4, basename as basename2, normalize as normalize2, resolve as resolve3, sep as sep2 } from "path";
|
|
248
|
+
import { mkdir, cp, access, readdir as readdir2, symlink, lstat, rm as rm2, readlink } from "fs/promises";
|
|
249
|
+
import { join as join4, basename as basename2, normalize as normalize2, resolve as resolve3, sep as sep2, relative } from "path";
|
|
250
|
+
import { homedir as homedir2, platform } from "os";
|
|
250
251
|
|
|
251
252
|
// src/agents.ts
|
|
252
253
|
import { homedir } from "os";
|
|
@@ -410,6 +411,8 @@ async function detectInstalledAgents() {
|
|
|
410
411
|
}
|
|
411
412
|
|
|
412
413
|
// src/installer.ts
|
|
414
|
+
var AGENTS_DIR = ".agents";
|
|
415
|
+
var SKILLS_SUBDIR = "skills";
|
|
413
416
|
function sanitizeName(name) {
|
|
414
417
|
let sanitized = name.replace(/[\/\\:\0]/g, "");
|
|
415
418
|
sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, "");
|
|
@@ -427,27 +430,107 @@ function isPathSafe(basePath, targetPath) {
|
|
|
427
430
|
const normalizedTarget = normalize2(resolve3(targetPath));
|
|
428
431
|
return normalizedTarget.startsWith(normalizedBase + sep2) || normalizedTarget === normalizedBase;
|
|
429
432
|
}
|
|
433
|
+
function getCanonicalSkillsDir(global, cwd) {
|
|
434
|
+
const baseDir = global ? homedir2() : cwd || process.cwd();
|
|
435
|
+
return join4(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
|
|
436
|
+
}
|
|
437
|
+
async function createSymlink(target, linkPath) {
|
|
438
|
+
try {
|
|
439
|
+
try {
|
|
440
|
+
const stats = await lstat(linkPath);
|
|
441
|
+
if (stats.isSymbolicLink()) {
|
|
442
|
+
const existingTarget = await readlink(linkPath);
|
|
443
|
+
if (resolve3(existingTarget) === resolve3(target)) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
await rm2(linkPath);
|
|
447
|
+
} else {
|
|
448
|
+
await rm2(linkPath, { recursive: true });
|
|
449
|
+
}
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") {
|
|
452
|
+
try {
|
|
453
|
+
await rm2(linkPath, { force: true });
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const linkDir = join4(linkPath, "..");
|
|
459
|
+
await mkdir(linkDir, { recursive: true });
|
|
460
|
+
const relativePath = relative(linkDir, target);
|
|
461
|
+
const symlinkType = platform() === "win32" ? "junction" : void 0;
|
|
462
|
+
await symlink(relativePath, linkPath, symlinkType);
|
|
463
|
+
return true;
|
|
464
|
+
} catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
430
468
|
async function installSkillForAgent(skill, agentType, options = {}) {
|
|
431
469
|
const agent = agents[agentType];
|
|
470
|
+
const isGlobal = options.global ?? false;
|
|
471
|
+
const cwd = options.cwd || process.cwd();
|
|
432
472
|
const rawSkillName = skill.name || basename2(skill.path);
|
|
433
473
|
const skillName = sanitizeName(rawSkillName);
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
474
|
+
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
475
|
+
const canonicalDir = join4(canonicalBase, skillName);
|
|
476
|
+
const agentBase = isGlobal ? agent.globalSkillsDir : join4(cwd, agent.skillsDir);
|
|
477
|
+
const agentDir = join4(agentBase, skillName);
|
|
478
|
+
const installMode = options.mode ?? "symlink";
|
|
479
|
+
if (!isPathSafe(canonicalBase, canonicalDir)) {
|
|
437
480
|
return {
|
|
438
481
|
success: false,
|
|
439
|
-
path:
|
|
482
|
+
path: agentDir,
|
|
483
|
+
mode: installMode,
|
|
484
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (!isPathSafe(agentBase, agentDir)) {
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
path: agentDir,
|
|
491
|
+
mode: installMode,
|
|
440
492
|
error: "Invalid skill name: potential path traversal detected"
|
|
441
493
|
};
|
|
442
494
|
}
|
|
443
495
|
try {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
496
|
+
if (installMode === "copy") {
|
|
497
|
+
await mkdir(agentDir, { recursive: true });
|
|
498
|
+
await copyDirectory(skill.path, agentDir);
|
|
499
|
+
return {
|
|
500
|
+
success: true,
|
|
501
|
+
path: agentDir,
|
|
502
|
+
mode: "copy"
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
await mkdir(canonicalDir, { recursive: true });
|
|
506
|
+
await copyDirectory(skill.path, canonicalDir);
|
|
507
|
+
const symlinkCreated = await createSymlink(canonicalDir, agentDir);
|
|
508
|
+
if (!symlinkCreated) {
|
|
509
|
+
try {
|
|
510
|
+
await rm2(agentDir, { recursive: true, force: true });
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
await mkdir(agentDir, { recursive: true });
|
|
514
|
+
await copyDirectory(skill.path, agentDir);
|
|
515
|
+
return {
|
|
516
|
+
success: true,
|
|
517
|
+
path: agentDir,
|
|
518
|
+
canonicalPath: canonicalDir,
|
|
519
|
+
mode: "symlink",
|
|
520
|
+
symlinkFailed: true
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
path: agentDir,
|
|
526
|
+
canonicalPath: canonicalDir,
|
|
527
|
+
mode: "symlink"
|
|
528
|
+
};
|
|
447
529
|
} catch (error) {
|
|
448
530
|
return {
|
|
449
531
|
success: false,
|
|
450
|
-
path:
|
|
532
|
+
path: agentDir,
|
|
533
|
+
mode: installMode,
|
|
451
534
|
error: error instanceof Error ? error.message : "Unknown error"
|
|
452
535
|
};
|
|
453
536
|
}
|
|
@@ -492,17 +575,19 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
|
492
575
|
return false;
|
|
493
576
|
}
|
|
494
577
|
}
|
|
495
|
-
function
|
|
496
|
-
const agent = agents[agentType];
|
|
578
|
+
function getCanonicalPath(skillName, options = {}) {
|
|
497
579
|
const sanitized = sanitizeName(skillName);
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
if (!isPathSafe(
|
|
580
|
+
const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
|
|
581
|
+
const canonicalPath = join4(canonicalBase, sanitized);
|
|
582
|
+
if (!isPathSafe(canonicalBase, canonicalPath)) {
|
|
501
583
|
throw new Error("Invalid skill name: potential path traversal detected");
|
|
502
584
|
}
|
|
503
|
-
return
|
|
585
|
+
return canonicalPath;
|
|
504
586
|
}
|
|
505
587
|
|
|
588
|
+
// src/index.ts
|
|
589
|
+
import { homedir as homedir3 } from "os";
|
|
590
|
+
|
|
506
591
|
// src/telemetry.ts
|
|
507
592
|
var TELEMETRY_URL = "https://add-skill.vercel.sh/t";
|
|
508
593
|
var cliVersion = null;
|
|
@@ -539,7 +624,7 @@ function track(data) {
|
|
|
539
624
|
// package.json
|
|
540
625
|
var package_default = {
|
|
541
626
|
name: "add-skill",
|
|
542
|
-
version: "1.0.
|
|
627
|
+
version: "1.0.23",
|
|
543
628
|
description: "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
|
|
544
629
|
type: "module",
|
|
545
630
|
bin: {
|
|
@@ -605,6 +690,24 @@ var package_default = {
|
|
|
605
690
|
};
|
|
606
691
|
|
|
607
692
|
// src/index.ts
|
|
693
|
+
function shortenPath(fullPath, cwd) {
|
|
694
|
+
const home2 = homedir3();
|
|
695
|
+
if (fullPath.startsWith(home2)) {
|
|
696
|
+
return fullPath.replace(home2, "~");
|
|
697
|
+
}
|
|
698
|
+
if (fullPath.startsWith(cwd)) {
|
|
699
|
+
return "." + fullPath.slice(cwd.length);
|
|
700
|
+
}
|
|
701
|
+
return fullPath;
|
|
702
|
+
}
|
|
703
|
+
function formatList(items, maxShow = 5) {
|
|
704
|
+
if (items.length <= maxShow) {
|
|
705
|
+
return items.join(", ");
|
|
706
|
+
}
|
|
707
|
+
const shown = items.slice(0, maxShow);
|
|
708
|
+
const remaining = items.length - maxShow;
|
|
709
|
+
return `${shown.join(", ")} +${remaining} more`;
|
|
710
|
+
}
|
|
608
711
|
var version = package_default.version;
|
|
609
712
|
setVersion(version);
|
|
610
713
|
program.name("add-skill").description("Install skills onto coding agents (OpenCode, Claude Code, Codex, Kiro CLI, Cursor, Antigravity, Github Copilot, Roo Code)").version(version).argument("<source>", "Git repo URL, GitHub shorthand (owner/repo), local path (./path), or direct path to skill").option("-g, --global", "Install skill globally (user-level) instead of project-level").option("-a, --agent <agents...>", "Specify agents to install to (opencode, claude-code, codex, kiro-cli, cursor, antigravity, github-copilot, roo)").option("-s, --skill <skills...>", "Specify skill names to install (skip selection prompt)").option("-l, --list", "List available skills in the repository without installing").option("-y, --yes", "Skip confirmation prompts").option("--all", "Install all skills to all agents without any prompts (implies -y -g)").configureOutput({
|
|
@@ -808,8 +911,24 @@ async function main(source, options) {
|
|
|
808
911
|
}
|
|
809
912
|
installGlobally = scope;
|
|
810
913
|
}
|
|
914
|
+
let installMode = "symlink";
|
|
915
|
+
if (!options.yes) {
|
|
916
|
+
const modeChoice = await p.select({
|
|
917
|
+
message: "Installation method",
|
|
918
|
+
options: [
|
|
919
|
+
{ value: "symlink", label: "Symlink (Recommended)", hint: "Single source of truth, easy updates" },
|
|
920
|
+
{ value: "copy", label: "Copy to all agents", hint: "Independent copies for each agent" }
|
|
921
|
+
]
|
|
922
|
+
});
|
|
923
|
+
if (p.isCancel(modeChoice)) {
|
|
924
|
+
p.cancel("Installation cancelled");
|
|
925
|
+
await cleanup(tempDir);
|
|
926
|
+
process.exit(0);
|
|
927
|
+
}
|
|
928
|
+
installMode = modeChoice;
|
|
929
|
+
}
|
|
930
|
+
const cwd = process.cwd();
|
|
811
931
|
const summaryLines = [];
|
|
812
|
-
const maxAgentLen = Math.max(...targetAgents.map((a) => agents[a].displayName.length));
|
|
813
932
|
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
814
933
|
for (const skill of selectedSkills) {
|
|
815
934
|
const agentStatus = /* @__PURE__ */ new Map();
|
|
@@ -818,18 +937,25 @@ async function main(source, options) {
|
|
|
818
937
|
}
|
|
819
938
|
overwriteStatus.set(skill.name, agentStatus);
|
|
820
939
|
}
|
|
940
|
+
const agentNames = targetAgents.map((a) => agents[a].displayName);
|
|
941
|
+
const hasOverwrites = Array.from(overwriteStatus.values()).some(
|
|
942
|
+
(agentMap) => Array.from(agentMap.values()).some((v) => v)
|
|
943
|
+
);
|
|
821
944
|
for (const skill of selectedSkills) {
|
|
822
945
|
if (summaryLines.length > 0) summaryLines.push("");
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
946
|
+
if (installMode === "symlink") {
|
|
947
|
+
const canonicalPath = getCanonicalPath(skill.name, { global: installGlobally });
|
|
948
|
+
const shortCanonical = shortenPath(canonicalPath, cwd);
|
|
949
|
+
summaryLines.push(`${chalk.cyan(shortCanonical)}`);
|
|
950
|
+
summaryLines.push(` ${chalk.dim("symlink \u2192")} ${formatList(agentNames)}`);
|
|
951
|
+
} else {
|
|
952
|
+
summaryLines.push(`${chalk.cyan(getSkillDisplayName(skill))}`);
|
|
953
|
+
summaryLines.push(` ${chalk.dim("copy \u2192")} ${formatList(agentNames)}`);
|
|
954
|
+
}
|
|
955
|
+
const skillOverwrites = overwriteStatus.get(skill.name);
|
|
956
|
+
const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
|
|
957
|
+
if (overwriteAgents.length > 0) {
|
|
958
|
+
summaryLines.push(` ${chalk.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
|
|
833
959
|
}
|
|
834
960
|
}
|
|
835
961
|
console.log();
|
|
@@ -846,7 +972,7 @@ async function main(source, options) {
|
|
|
846
972
|
const results = [];
|
|
847
973
|
for (const skill of selectedSkills) {
|
|
848
974
|
for (const agent of targetAgents) {
|
|
849
|
-
const result = await installSkillForAgent(skill, agent, { global: installGlobally });
|
|
975
|
+
const result = await installSkillForAgent(skill, agent, { global: installGlobally, mode: installMode });
|
|
850
976
|
results.push({
|
|
851
977
|
skill: getSkillDisplayName(skill),
|
|
852
978
|
agent: agents[agent].displayName,
|
|
@@ -884,25 +1010,44 @@ async function main(source, options) {
|
|
|
884
1010
|
if (successful.length > 0) {
|
|
885
1011
|
const bySkill = /* @__PURE__ */ new Map();
|
|
886
1012
|
for (const r of successful) {
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
bySkill.set(r.skill,
|
|
1013
|
+
const skillResults = bySkill.get(r.skill) || [];
|
|
1014
|
+
skillResults.push(r);
|
|
1015
|
+
bySkill.set(r.skill, skillResults);
|
|
890
1016
|
}
|
|
891
1017
|
const skillCount = bySkill.size;
|
|
892
1018
|
const agentCount = new Set(successful.map((r) => r.agent)).size;
|
|
1019
|
+
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
1020
|
+
const copiedAgents = symlinkFailures.map((r) => r.agent);
|
|
893
1021
|
const resultLines = [];
|
|
894
|
-
for (const [
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
resultLines.push(
|
|
1022
|
+
for (const [skillName, skillResults] of bySkill) {
|
|
1023
|
+
const firstResult = skillResults[0];
|
|
1024
|
+
if (firstResult.mode === "copy") {
|
|
1025
|
+
resultLines.push(`${chalk.green("\u2713")} ${skillName} ${chalk.dim("(copied)")}`);
|
|
1026
|
+
for (const r of skillResults) {
|
|
1027
|
+
const shortPath = shortenPath(r.path, cwd);
|
|
1028
|
+
resultLines.push(` ${chalk.dim("\u2192")} ${shortPath}`);
|
|
1029
|
+
}
|
|
1030
|
+
} else {
|
|
1031
|
+
if (firstResult.canonicalPath) {
|
|
1032
|
+
const shortPath = shortenPath(firstResult.canonicalPath, cwd);
|
|
1033
|
+
resultLines.push(`${chalk.green("\u2713")} ${shortPath}`);
|
|
1034
|
+
}
|
|
1035
|
+
const symlinked = skillResults.filter((r) => !r.symlinkFailed).map((r) => r.agent);
|
|
1036
|
+
const copied = skillResults.filter((r) => r.symlinkFailed).map((r) => r.agent);
|
|
1037
|
+
if (symlinked.length > 0) {
|
|
1038
|
+
resultLines.push(` ${chalk.dim("symlink \u2192")} ${formatList(symlinked)}`);
|
|
1039
|
+
}
|
|
1040
|
+
if (copied.length > 0) {
|
|
1041
|
+
resultLines.push(` ${chalk.yellow("copied \u2192")} ${formatList(copied)}`);
|
|
1042
|
+
}
|
|
898
1043
|
}
|
|
899
|
-
resultLines.push("");
|
|
900
|
-
}
|
|
901
|
-
if (resultLines.length > 0 && resultLines[resultLines.length - 1] === "") {
|
|
902
|
-
resultLines.pop();
|
|
903
1044
|
}
|
|
904
1045
|
const title = chalk.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""}`);
|
|
905
1046
|
p.note(resultLines.join("\n"), title);
|
|
1047
|
+
if (symlinkFailures.length > 0) {
|
|
1048
|
+
p.log.warn(chalk.yellow(`Symlinks failed for: ${formatList(copiedAgents)}`));
|
|
1049
|
+
p.log.message(chalk.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
|
|
1050
|
+
}
|
|
906
1051
|
}
|
|
907
1052
|
if (failed.length > 0) {
|
|
908
1053
|
console.log();
|