add-skill 1.0.14 → 1.0.16

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 +92 -41
  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";
@@ -318,15 +334,6 @@ var agents = {
318
334
  return existsSync(join3(home, ".factory/skills"));
319
335
  }
320
336
  },
321
- gemini: {
322
- name: "gemini",
323
- displayName: "Gemini CLI",
324
- skillsDir: ".gemini/skills",
325
- globalSkillsDir: join3(home, ".gemini/skills"),
326
- detectInstalled: async () => {
327
- return existsSync(join3(home, ".gemini"));
328
- }
329
- },
330
337
  windsurf: {
331
338
  name: "windsurf",
332
339
  displayName: "Windsurf",
@@ -361,8 +368,8 @@ function sanitizeName(name) {
361
368
  return sanitized;
362
369
  }
363
370
  function isPathSafe(basePath, targetPath) {
364
- const normalizedBase = normalize2(resolve2(basePath));
365
- const normalizedTarget = normalize2(resolve2(targetPath));
371
+ const normalizedBase = normalize2(resolve3(basePath));
372
+ const normalizedTarget = normalize2(resolve3(targetPath));
366
373
  return normalizedTarget.startsWith(normalizedBase + sep2) || normalizedTarget === normalizedBase;
367
374
  }
368
375
  async function installSkillForAgent(skill, agentType, options = {}) {
@@ -477,7 +484,7 @@ function track(data) {
477
484
  // package.json
478
485
  var package_default = {
479
486
  name: "add-skill",
480
- version: "1.0.14",
487
+ version: "1.0.16",
481
488
  description: "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
482
489
  type: "module",
483
490
  bin: {
@@ -536,7 +543,7 @@ var package_default = {
536
543
  // src/index.ts
537
544
  var version = package_default.version;
538
545
  setVersion(version);
539
- 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({
540
547
  outputError: (str, write) => {
541
548
  if (str.includes("missing required argument")) {
542
549
  console.log();
@@ -566,12 +573,26 @@ async function main(source, options) {
566
573
  const spinner2 = p.spinner();
567
574
  spinner2.start("Parsing source...");
568
575
  const parsed = parseSource(source);
569
- spinner2.stop(`Source: ${chalk.cyan(parsed.url)}${parsed.subpath ? ` (${parsed.subpath})` : ""}`);
570
- spinner2.start("Cloning repository...");
571
- tempDir = await cloneRepo(parsed.url);
572
- 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
+ }
573
594
  spinner2.start("Discovering skills...");
574
- const skills = await discoverSkills(tempDir, parsed.subpath);
595
+ const skills = await discoverSkills(skillsDir, parsed.subpath);
575
596
  if (skills.length === 0) {
576
597
  spinner2.stop(chalk.red("No skills found"));
577
598
  p.outro(chalk.red("No valid skills found. Skills require a SKILL.md with name and description."));
@@ -662,7 +683,8 @@ async function main(source, options) {
662
683
  const selected = await p.multiselect({
663
684
  message: "Select agents to install skills to",
664
685
  options: allAgentChoices,
665
- required: true
686
+ required: true,
687
+ initialValues: Object.keys(agents)
666
688
  });
667
689
  if (p.isCancel(selected)) {
668
690
  p.cancel("Installation cancelled");
@@ -715,18 +737,32 @@ async function main(source, options) {
715
737
  }
716
738
  installGlobally = scope;
717
739
  }
718
- console.log();
719
- 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();
743
+ for (const skill of selectedSkills) {
744
+ const agentStatus = /* @__PURE__ */ new Map();
745
+ for (const agent of targetAgents) {
746
+ agentStatus.set(agent, await isSkillInstalled(skill.name, agent, { global: installGlobally }));
747
+ }
748
+ overwriteStatus.set(skill.name, agentStatus);
749
+ }
720
750
  for (const skill of selectedSkills) {
721
- p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
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")}`);
722
755
  for (const agent of targetAgents) {
723
- const path = getInstallPath(skill.name, agent, { global: installGlobally });
724
- const installed = await isSkillInstalled(skill.name, agent, { global: installGlobally });
725
- const status = installed ? chalk.yellow(" (will overwrite)") : "";
726
- p.log.message(` ${chalk.dim("\u2192")} ${agents[agent].displayName}: ${chalk.dim(path)}${status}`);
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}`);
727
762
  }
728
763
  }
729
764
  console.log();
765
+ p.note(summaryLines.join("\n"), "Installation Summary");
730
766
  if (!options.yes) {
731
767
  const confirmed = await p.confirm({ message: "Proceed with installation?" });
732
768
  if (p.isCancel(confirmed) || !confirmed) {
@@ -765,18 +801,33 @@ async function main(source, options) {
765
801
  skillFiles: JSON.stringify(skillFiles)
766
802
  });
767
803
  if (successful.length > 0) {
768
- p.log.success(chalk.green(`Successfully installed ${successful.length} skill${successful.length !== 1 ? "s" : ""}`));
804
+ const bySkill = /* @__PURE__ */ new Map();
769
805
  for (const r of successful) {
770
- p.log.message(` ${chalk.green("\u2713")} ${r.skill} \u2192 ${r.agent}`);
771
- p.log.message(` ${chalk.dim(r.path)}`);
806
+ const skillAgents = bySkill.get(r.skill) || [];
807
+ skillAgents.push(r.agent);
808
+ bySkill.set(r.skill, skillAgents);
809
+ }
810
+ const skillCount = bySkill.size;
811
+ const agentCount = new Set(successful.map((r) => r.agent)).size;
812
+ const resultLines = [];
813
+ for (const [skill, skillAgents] of bySkill) {
814
+ resultLines.push(`${chalk.green("\u2713")} ${chalk.bold(skill)}`);
815
+ for (const agent of skillAgents) {
816
+ resultLines.push(` ${chalk.dim(agent)}`);
817
+ }
818
+ resultLines.push("");
819
+ }
820
+ if (resultLines.length > 0 && resultLines[resultLines.length - 1] === "") {
821
+ resultLines.pop();
772
822
  }
823
+ const title = chalk.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""}`);
824
+ p.note(resultLines.join("\n"), title);
773
825
  }
774
826
  if (failed.length > 0) {
775
827
  console.log();
776
- p.log.error(chalk.red(`Failed to install ${failed.length} skill${failed.length !== 1 ? "s" : ""}`));
828
+ p.log.error(chalk.red(`Failed to install ${failed.length}`));
777
829
  for (const r of failed) {
778
- p.log.message(` ${chalk.red("\u2717")} ${r.skill} \u2192 ${r.agent}`);
779
- p.log.message(` ${chalk.dim(r.error)}`);
830
+ p.log.message(` ${chalk.red("\u2717")} ${r.skill} \u2192 ${r.agent}: ${chalk.dim(r.error)}`);
780
831
  }
781
832
  }
782
833
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-skill",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "bin": {