add-skill 1.0.21 → 1.0.22

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 +130 -39
  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,82 @@ 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 {
451
+ }
452
+ const linkDir = join4(linkPath, "..");
453
+ await mkdir(linkDir, { recursive: true });
454
+ const relativePath = relative(linkDir, target);
455
+ const symlinkType = platform() === "win32" ? "junction" : void 0;
456
+ await symlink(relativePath, linkPath, symlinkType);
457
+ return true;
458
+ } catch {
459
+ return false;
460
+ }
461
+ }
430
462
  async function installSkillForAgent(skill, agentType, options = {}) {
431
463
  const agent = agents[agentType];
464
+ const isGlobal = options.global ?? false;
465
+ const cwd = options.cwd || process.cwd();
432
466
  const rawSkillName = skill.name || basename2(skill.path);
433
467
  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)) {
468
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
469
+ const canonicalDir = join4(canonicalBase, skillName);
470
+ const agentBase = isGlobal ? agent.globalSkillsDir : join4(cwd, agent.skillsDir);
471
+ const agentDir = join4(agentBase, skillName);
472
+ if (!isPathSafe(canonicalBase, canonicalDir)) {
473
+ return {
474
+ success: false,
475
+ path: agentDir,
476
+ error: "Invalid skill name: potential path traversal detected"
477
+ };
478
+ }
479
+ if (!isPathSafe(agentBase, agentDir)) {
437
480
  return {
438
481
  success: false,
439
- path: targetDir,
482
+ path: agentDir,
440
483
  error: "Invalid skill name: potential path traversal detected"
441
484
  };
442
485
  }
443
486
  try {
444
- await mkdir(targetDir, { recursive: true });
445
- await copyDirectory(skill.path, targetDir);
446
- return { success: true, path: targetDir };
487
+ await mkdir(canonicalDir, { recursive: true });
488
+ await copyDirectory(skill.path, canonicalDir);
489
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
490
+ if (!symlinkCreated) {
491
+ await mkdir(agentDir, { recursive: true });
492
+ await copyDirectory(skill.path, agentDir);
493
+ return {
494
+ success: true,
495
+ path: agentDir,
496
+ canonicalPath: canonicalDir,
497
+ symlinkFailed: true
498
+ };
499
+ }
500
+ return {
501
+ success: true,
502
+ path: agentDir,
503
+ canonicalPath: canonicalDir
504
+ };
447
505
  } catch (error) {
448
506
  return {
449
507
  success: false,
450
- path: targetDir,
508
+ path: agentDir,
451
509
  error: error instanceof Error ? error.message : "Unknown error"
452
510
  };
453
511
  }
@@ -492,17 +550,19 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
492
550
  return false;
493
551
  }
494
552
  }
495
- function getInstallPath(skillName, agentType, options = {}) {
496
- const agent = agents[agentType];
553
+ function getCanonicalPath(skillName, options = {}) {
497
554
  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)) {
555
+ const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
556
+ const canonicalPath = join4(canonicalBase, sanitized);
557
+ if (!isPathSafe(canonicalBase, canonicalPath)) {
501
558
  throw new Error("Invalid skill name: potential path traversal detected");
502
559
  }
503
- return installPath;
560
+ return canonicalPath;
504
561
  }
505
562
 
563
+ // src/index.ts
564
+ import { homedir as homedir3 } from "os";
565
+
506
566
  // src/telemetry.ts
507
567
  var TELEMETRY_URL = "https://add-skill.vercel.sh/t";
508
568
  var cliVersion = null;
@@ -539,7 +599,7 @@ function track(data) {
539
599
  // package.json
540
600
  var package_default = {
541
601
  name: "add-skill",
542
- version: "1.0.21",
602
+ version: "1.0.22",
543
603
  description: "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
544
604
  type: "module",
545
605
  bin: {
@@ -605,6 +665,24 @@ var package_default = {
605
665
  };
606
666
 
607
667
  // src/index.ts
668
+ function shortenPath(fullPath, cwd) {
669
+ const home2 = homedir3();
670
+ if (fullPath.startsWith(home2)) {
671
+ return fullPath.replace(home2, "~");
672
+ }
673
+ if (fullPath.startsWith(cwd)) {
674
+ return "." + fullPath.slice(cwd.length);
675
+ }
676
+ return fullPath;
677
+ }
678
+ function formatList(items, maxShow = 5) {
679
+ if (items.length <= maxShow) {
680
+ return items.join(", ");
681
+ }
682
+ const shown = items.slice(0, maxShow);
683
+ const remaining = items.length - maxShow;
684
+ return `${shown.join(", ")} +${remaining} more`;
685
+ }
608
686
  var version = package_default.version;
609
687
  setVersion(version);
610
688
  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 +886,8 @@ async function main(source, options) {
808
886
  }
809
887
  installGlobally = scope;
810
888
  }
889
+ const cwd = process.cwd();
811
890
  const summaryLines = [];
812
- const maxAgentLen = Math.max(...targetAgents.map((a) => agents[a].displayName.length));
813
891
  const overwriteStatus = /* @__PURE__ */ new Map();
814
892
  for (const skill of selectedSkills) {
815
893
  const agentStatus = /* @__PURE__ */ new Map();
@@ -818,18 +896,20 @@ async function main(source, options) {
818
896
  }
819
897
  overwriteStatus.set(skill.name, agentStatus);
820
898
  }
899
+ const agentNames = targetAgents.map((a) => agents[a].displayName);
900
+ const hasOverwrites = Array.from(overwriteStatus.values()).some(
901
+ (agentMap) => Array.from(agentMap.values()).some((v) => v)
902
+ );
821
903
  for (const skill of selectedSkills) {
822
904
  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}`);
905
+ const canonicalPath = getCanonicalPath(skill.name, { global: installGlobally });
906
+ const shortCanonical = shortenPath(canonicalPath, cwd);
907
+ summaryLines.push(`${chalk.cyan(shortCanonical)}`);
908
+ const skillOverwrites = overwriteStatus.get(skill.name);
909
+ const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
910
+ summaryLines.push(` ${chalk.dim("\u2192")} ${formatList(agentNames)}`);
911
+ if (overwriteAgents.length > 0) {
912
+ summaryLines.push(` ${chalk.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
833
913
  }
834
914
  }
835
915
  console.log();
@@ -884,25 +964,36 @@ async function main(source, options) {
884
964
  if (successful.length > 0) {
885
965
  const bySkill = /* @__PURE__ */ new Map();
886
966
  for (const r of successful) {
887
- const skillAgents = bySkill.get(r.skill) || [];
888
- skillAgents.push(r.agent);
889
- bySkill.set(r.skill, skillAgents);
967
+ const skillResults = bySkill.get(r.skill) || [];
968
+ skillResults.push(r);
969
+ bySkill.set(r.skill, skillResults);
890
970
  }
891
971
  const skillCount = bySkill.size;
892
972
  const agentCount = new Set(successful.map((r) => r.agent)).size;
973
+ const symlinkFailures = successful.filter((r) => r.symlinkFailed);
974
+ const copiedAgents = symlinkFailures.map((r) => r.agent);
893
975
  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)}`);
976
+ for (const [, skillResults] of bySkill) {
977
+ const firstResult = skillResults[0];
978
+ if (firstResult.canonicalPath) {
979
+ const shortPath = shortenPath(firstResult.canonicalPath, cwd);
980
+ resultLines.push(`${chalk.green("\u2713")} ${shortPath}`);
981
+ }
982
+ const symlinked = skillResults.filter((r) => !r.symlinkFailed).map((r) => r.agent);
983
+ const copied = skillResults.filter((r) => r.symlinkFailed).map((r) => r.agent);
984
+ if (symlinked.length > 0) {
985
+ resultLines.push(` ${chalk.dim("\u2192")} ${formatList(symlinked)}`);
986
+ }
987
+ if (copied.length > 0) {
988
+ resultLines.push(` ${chalk.yellow("copied \u2192")} ${formatList(copied)}`);
898
989
  }
899
- resultLines.push("");
900
- }
901
- if (resultLines.length > 0 && resultLines[resultLines.length - 1] === "") {
902
- resultLines.pop();
903
990
  }
904
991
  const title = chalk.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""}`);
905
992
  p.note(resultLines.join("\n"), title);
993
+ if (symlinkFailures.length > 0) {
994
+ p.log.warn(chalk.yellow(`Symlinks failed for: ${formatList(copiedAgents)}`));
995
+ p.log.message(chalk.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
996
+ }
906
997
  }
907
998
  if (failed.length > 0) {
908
999
  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.22",
4
4
  "description": "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "bin": {