add-skill 1.0.15 → 1.0.17

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 (3) hide show
  1. package/README.md +1 -2
  2. package/dist/index.js +95 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Install agent skills onto your coding agents from any git repository.
4
4
 
5
5
  <!-- agent-list:start -->
6
- Supports **Opencode**, **Claude Code**, **Codex**, **Cursor**, and [11 more](#available-agents).
6
+ Supports **Opencode**, **Claude Code**, **Codex**, **Cursor**, and [10 more](#available-agents).
7
7
  <!-- agent-list:end -->
8
8
 
9
9
  ## Quick Start
@@ -95,7 +95,6 @@ Skills can be installed to any of these supported agents. Use `-g, --global` to
95
95
  | GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` |
96
96
  | Clawdbot | `skills/` | `~/.clawdbot/skills/` |
97
97
  | Droid | `.factory/skills/` | `~/.factory/skills/` |
98
- | Gemini CLI | `.gemini/skills/` | `~/.gemini/skills/` |
99
98
  | Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` |
100
99
  <!-- available-agents:end -->
101
100
 
package/dist/index.js CHANGED
@@ -5,12 +5,22 @@ import { program } from "commander";
5
5
  import * as p from "@clack/prompts";
6
6
  import chalk from "chalk";
7
7
 
8
- // src/git.ts
9
- import simpleGit from "simple-git";
10
- import { join, normalize, resolve, sep } from "path";
11
- import { mkdtemp, rm } from "fs/promises";
12
- import { tmpdir } from "os";
8
+ // src/source-parser.ts
9
+ import { isAbsolute, resolve } from "path";
10
+ function isLocalPath(input) {
11
+ return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || // Windows absolute paths like C:\ or D:\
12
+ /^[a-zA-Z]:[/\\]/.test(input);
13
+ }
13
14
  function parseSource(input) {
15
+ if (isLocalPath(input)) {
16
+ const resolvedPath = resolve(input);
17
+ return {
18
+ type: "local",
19
+ url: resolvedPath,
20
+ // Store resolved path in url for consistency
21
+ localPath: resolvedPath
22
+ };
23
+ }
14
24
  const githubTreeMatch = input.match(
15
25
  /github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/
16
26
  );
@@ -52,7 +62,7 @@ function parseSource(input) {
52
62
  };
53
63
  }
54
64
  const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
55
- if (shorthandMatch && !input.includes(":")) {
65
+ if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
56
66
  const [, owner, repo, subpath] = shorthandMatch;
57
67
  return {
58
68
  type: "github",
@@ -65,6 +75,12 @@ function parseSource(input) {
65
75
  url: input
66
76
  };
67
77
  }
78
+
79
+ // src/git.ts
80
+ import simpleGit from "simple-git";
81
+ import { join, normalize, resolve as resolve2, sep } from "path";
82
+ import { mkdtemp, rm } from "fs/promises";
83
+ import { tmpdir } from "os";
68
84
  async function cloneRepo(url) {
69
85
  const tempDir = await mkdtemp(join(tmpdir(), "add-skill-"));
70
86
  const git = simpleGit();
@@ -72,8 +88,8 @@ async function cloneRepo(url) {
72
88
  return tempDir;
73
89
  }
74
90
  async function cleanupTempDir(dir) {
75
- const normalizedDir = normalize(resolve(dir));
76
- const normalizedTmpDir = normalize(resolve(tmpdir()));
91
+ const normalizedDir = normalize(resolve2(dir));
92
+ const normalizedTmpDir = normalize(resolve2(tmpdir()));
77
93
  if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) {
78
94
  throw new Error("Attempted to clean up directory outside of temp directory");
79
95
  }
@@ -193,7 +209,7 @@ function getSkillDisplayName(skill) {
193
209
 
194
210
  // src/installer.ts
195
211
  import { mkdir, cp, access, readdir as readdir2 } from "fs/promises";
196
- import { join as join4, basename as basename2, normalize as normalize2, resolve as resolve2, sep as sep2 } from "path";
212
+ import { join as join4, basename as basename2, normalize as normalize2, resolve as resolve3, sep as sep2 } from "path";
197
213
 
198
214
  // src/agents.ts
199
215
  import { homedir } from "os";
@@ -352,8 +368,8 @@ function sanitizeName(name) {
352
368
  return sanitized;
353
369
  }
354
370
  function isPathSafe(basePath, targetPath) {
355
- const normalizedBase = normalize2(resolve2(basePath));
356
- const normalizedTarget = normalize2(resolve2(targetPath));
371
+ const normalizedBase = normalize2(resolve3(basePath));
372
+ const normalizedTarget = normalize2(resolve3(targetPath));
357
373
  return normalizedTarget.startsWith(normalizedBase + sep2) || normalizedTarget === normalizedBase;
358
374
  }
359
375
  async function installSkillForAgent(skill, agentType, options = {}) {
@@ -468,7 +484,7 @@ function track(data) {
468
484
  // package.json
469
485
  var package_default = {
470
486
  name: "add-skill",
471
- version: "1.0.15",
487
+ version: "1.0.17",
472
488
  description: "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
473
489
  type: "module",
474
490
  bin: {
@@ -527,7 +543,7 @@ var package_default = {
527
543
  // src/index.ts
528
544
  var version = package_default.version;
529
545
  setVersion(version);
530
- program.name("add-skill").description("Install skills onto coding agents (OpenCode, Claude Code, Codex, Cursor, Antigravity, Github Copilot, Roo Code)").version(version).argument("<source>", "Git repo URL, GitHub shorthand (owner/repo), 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, cursor, antigravity, gitub-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").configureOutput({
546
+ program.name("add-skill").description("Install skills onto coding agents (OpenCode, Claude Code, Codex, 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, cursor, antigravity, gitub-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").configureOutput({
531
547
  outputError: (str, write) => {
532
548
  if (str.includes("missing required argument")) {
533
549
  console.log();
@@ -557,12 +573,26 @@ async function main(source, options) {
557
573
  const spinner2 = p.spinner();
558
574
  spinner2.start("Parsing source...");
559
575
  const parsed = parseSource(source);
560
- spinner2.stop(`Source: ${chalk.cyan(parsed.url)}${parsed.subpath ? ` (${parsed.subpath})` : ""}`);
561
- spinner2.start("Cloning repository...");
562
- tempDir = await cloneRepo(parsed.url);
563
- spinner2.stop("Repository cloned");
576
+ spinner2.stop(`Source: ${chalk.cyan(parsed.type === "local" ? parsed.localPath : parsed.url)}${parsed.subpath ? ` (${parsed.subpath})` : ""}`);
577
+ let skillsDir;
578
+ if (parsed.type === "local") {
579
+ spinner2.start("Validating local path...");
580
+ const { existsSync: existsSync2 } = await import("fs");
581
+ if (!existsSync2(parsed.localPath)) {
582
+ spinner2.stop(chalk.red("Path not found"));
583
+ p.outro(chalk.red(`Local path does not exist: ${parsed.localPath}`));
584
+ process.exit(1);
585
+ }
586
+ skillsDir = parsed.localPath;
587
+ spinner2.stop("Local path validated");
588
+ } else {
589
+ spinner2.start("Cloning repository...");
590
+ tempDir = await cloneRepo(parsed.url);
591
+ skillsDir = tempDir;
592
+ spinner2.stop("Repository cloned");
593
+ }
564
594
  spinner2.start("Discovering skills...");
565
- const skills = await discoverSkills(tempDir, parsed.subpath);
595
+ const skills = await discoverSkills(skillsDir, parsed.subpath);
566
596
  if (skills.length === 0) {
567
597
  spinner2.stop(chalk.red("No skills found"));
568
598
  p.outro(chalk.red("No valid skills found. Skills require a SKILL.md with name and description."));
@@ -653,7 +683,8 @@ async function main(source, options) {
653
683
  const selected = await p.multiselect({
654
684
  message: "Select agents to install skills to",
655
685
  options: allAgentChoices,
656
- required: true
686
+ required: true,
687
+ initialValues: Object.keys(agents)
657
688
  });
658
689
  if (p.isCancel(selected)) {
659
690
  p.cancel("Installation cancelled");
@@ -706,18 +737,32 @@ async function main(source, options) {
706
737
  }
707
738
  installGlobally = scope;
708
739
  }
709
- console.log();
710
- p.log.step(chalk.bold("Installation Summary"));
740
+ const summaryLines = [];
741
+ const maxAgentLen = Math.max(...targetAgents.map((a) => agents[a].displayName.length));
742
+ const overwriteStatus = /* @__PURE__ */ new Map();
711
743
  for (const skill of selectedSkills) {
712
- p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
744
+ const agentStatus = /* @__PURE__ */ new Map();
713
745
  for (const agent of targetAgents) {
714
- const path = getInstallPath(skill.name, agent, { global: installGlobally });
715
- const installed = await isSkillInstalled(skill.name, agent, { global: installGlobally });
716
- const status = installed ? chalk.yellow(" (will overwrite)") : "";
717
- p.log.message(` ${chalk.dim("\u2192")} ${agents[agent].displayName}: ${chalk.dim(path)}${status}`);
746
+ agentStatus.set(agent, await isSkillInstalled(skill.name, agent, { global: installGlobally }));
747
+ }
748
+ overwriteStatus.set(skill.name, agentStatus);
749
+ }
750
+ for (const skill of selectedSkills) {
751
+ if (summaryLines.length > 0) summaryLines.push("");
752
+ summaryLines.push(chalk.bold.cyan(getSkillDisplayName(skill)));
753
+ summaryLines.push("");
754
+ summaryLines.push(` ${chalk.bold("Agent".padEnd(maxAgentLen + 2))}${chalk.bold("Directory")}`);
755
+ for (const agent of targetAgents) {
756
+ const fullPath = getInstallPath(skill.name, agent, { global: installGlobally });
757
+ const basePath = fullPath.replace(/\/[^/]+$/, "/");
758
+ const installed = overwriteStatus.get(skill.name)?.get(agent) ?? false;
759
+ const status = installed ? chalk.yellow(" (overwrite)") : "";
760
+ const agentName = agents[agent].displayName.padEnd(maxAgentLen + 2);
761
+ summaryLines.push(` ${agentName}${chalk.dim(basePath)}${status}`);
718
762
  }
719
763
  }
720
764
  console.log();
765
+ p.note(summaryLines.join("\n"), "Installation Summary");
721
766
  if (!options.yes) {
722
767
  const confirmed = await p.confirm({ message: "Proceed with installation?" });
723
768
  if (p.isCancel(confirmed) || !confirmed) {
@@ -744,8 +789,15 @@ async function main(source, options) {
744
789
  const failed = results.filter((r) => !r.success);
745
790
  const skillFiles = {};
746
791
  for (const skill of selectedSkills) {
747
- const relativePath = skill.path.replace(tempDir + "/", "");
748
- skillFiles[skill.name] = relativePath + "/SKILL.md";
792
+ let relativePath;
793
+ if (tempDir && skill.path === tempDir) {
794
+ relativePath = "SKILL.md";
795
+ } else if (tempDir && skill.path.startsWith(tempDir + "/")) {
796
+ relativePath = skill.path.slice(tempDir.length + 1) + "/SKILL.md";
797
+ } else {
798
+ relativePath = skill.name + "/SKILL.md";
799
+ }
800
+ skillFiles[skill.name] = relativePath;
749
801
  }
750
802
  track({
751
803
  event: "install",
@@ -758,16 +810,25 @@ async function main(source, options) {
758
810
  if (successful.length > 0) {
759
811
  const bySkill = /* @__PURE__ */ new Map();
760
812
  for (const r of successful) {
761
- const agents2 = bySkill.get(r.skill) || [];
762
- agents2.push(r.agent);
763
- bySkill.set(r.skill, agents2);
813
+ const skillAgents = bySkill.get(r.skill) || [];
814
+ skillAgents.push(r.agent);
815
+ bySkill.set(r.skill, skillAgents);
764
816
  }
765
817
  const skillCount = bySkill.size;
766
818
  const agentCount = new Set(successful.map((r) => r.agent)).size;
767
- p.log.success(chalk.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""}`));
768
- for (const [skill, agents2] of bySkill) {
769
- p.log.message(` ${chalk.green("\u2713")} ${skill} ${chalk.dim("\u2192")} ${chalk.dim(agents2.join(", "))}`);
819
+ const resultLines = [];
820
+ for (const [skill, skillAgents] of bySkill) {
821
+ resultLines.push(`${chalk.green("\u2713")} ${chalk.bold(skill)}`);
822
+ for (const agent of skillAgents) {
823
+ resultLines.push(` ${chalk.dim(agent)}`);
824
+ }
825
+ resultLines.push("");
826
+ }
827
+ if (resultLines.length > 0 && resultLines[resultLines.length - 1] === "") {
828
+ resultLines.pop();
770
829
  }
830
+ const title = chalk.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""}`);
831
+ p.note(resultLines.join("\n"), title);
771
832
  }
772
833
  if (failed.length > 0) {
773
834
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-skill",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "bin": {