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.
Files changed (2) hide show
  1. package/dist/index.js +185 -40
  2. 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 targetBase = options.global ? agent.globalSkillsDir : join4(options.cwd || process.cwd(), agent.skillsDir);
435
- const targetDir = join4(targetBase, skillName);
436
- if (!isPathSafe(targetBase, targetDir)) {
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: targetDir,
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
- await mkdir(targetDir, { recursive: true });
445
- await copyDirectory(skill.path, targetDir);
446
- return { success: true, path: targetDir };
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: targetDir,
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 getInstallPath(skillName, agentType, options = {}) {
496
- const agent = agents[agentType];
578
+ function getCanonicalPath(skillName, options = {}) {
497
579
  const sanitized = sanitizeName(skillName);
498
- const targetBase = options.global ? agent.globalSkillsDir : join4(options.cwd || process.cwd(), agent.skillsDir);
499
- const installPath = join4(targetBase, sanitized);
500
- if (!isPathSafe(targetBase, installPath)) {
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 installPath;
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.21",
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
- summaryLines.push(chalk.bold.cyan(getSkillDisplayName(skill)));
824
- summaryLines.push("");
825
- summaryLines.push(` ${chalk.bold("Agent".padEnd(maxAgentLen + 2))}${chalk.bold("Directory")}`);
826
- for (const agent of targetAgents) {
827
- const fullPath = getInstallPath(skill.name, agent, { global: installGlobally });
828
- const basePath = fullPath.replace(/\/[^/]+$/, "/");
829
- const installed = overwriteStatus.get(skill.name)?.get(agent) ?? false;
830
- const status = installed ? chalk.yellow(" (overwrite)") : "";
831
- const agentName = agents[agent].displayName.padEnd(maxAgentLen + 2);
832
- summaryLines.push(` ${agentName}${chalk.dim(basePath)}${status}`);
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 skillAgents = bySkill.get(r.skill) || [];
888
- skillAgents.push(r.agent);
889
- bySkill.set(r.skill, skillAgents);
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 [skill, skillAgents] of bySkill) {
895
- resultLines.push(`${chalk.green("\u2713")} ${chalk.bold(skill)}`);
896
- for (const agent of skillAgents) {
897
- resultLines.push(` ${chalk.dim(agent)}`);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-skill",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "bin": {