@kurokeita/add-skill 1.2.0 → 1.4.0

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/README.md CHANGED
@@ -27,6 +27,35 @@ Starts an interactive session to select and install skills.
27
27
  pnpx @kurokeita/add-skill add
28
28
  ```
29
29
 
30
+ ### Add Skill from GitHub
31
+
32
+ Install a skill directly from a GitHub URL.
33
+
34
+ ```bash
35
+ pnpx @kurokeita/add-skill add https://github.com/owner/repo/tree/main/skills/skill-name
36
+ ```
37
+
38
+ ### Add new Skill to this repository (For Maintainers)
39
+
40
+ - Either use the import tool to import a skill from GitHub into the repository's `skills` directory or adding a skill yourself in the `skills` directory.
41
+
42
+ ```bash
43
+ pnpm dev import https://github.com/owner/repo/tree/main/skills/skill-name
44
+ ```
45
+
46
+ - Create a PR to merge the skill into the repository.
47
+
48
+ ## Supported Agents
49
+
50
+ <!-- SUPPORTED_AGENTS_START -->
51
+ | Agent | Global Path |
52
+ | :--- | :--- |
53
+ | Antigravity | `~/.gemini/antigravity/global_skills` |
54
+ | Gemini CLI | `~/.gemini/skills` |
55
+ | GitHub Copilot | `~/.copilot/skills` |
56
+ | Windsurf | `~/.codeium/windsurf/skills` |
57
+ <!-- SUPPORTED_AGENTS_END -->
58
+
30
59
  ## Development
31
60
 
32
61
  1. **Clone the repository:**
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { addSkill } from "../src/commands/add.js";
4
+ import { importSkill } from "../src/commands/import.js";
4
5
  import { listSkills } from "../src/commands/list.js";
5
6
  const program = new Command();
6
7
  program.name("skills").description("CLI to manage AI skills").version("1.0.0");
7
8
  program.command("list").description("List available skills").action(listSkills);
8
9
  program
9
- .command("add")
10
- .description("Add skills to platforms (Interactive)")
10
+ .command("add [url]")
11
+ .description("Add skills to platforms (Interactive or from GitHub URL)")
11
12
  .action(addSkill);
13
+ program
14
+ .command("import <url>")
15
+ .description("Import a skill from GitHub to the repo")
16
+ .action(importSkill);
12
17
  program.parse();
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: make-instruction
3
+ description: Generates custom Copilot instruction files (.instructions.md) following official best practices. Use when you need to standardize code generation, reviews, or documentation for specific domains or file types.
4
+ ---
5
+
6
+ # Make Instruction
7
+
8
+ This skill helps you generate custom instruction files for GitHub Copilot (`.github/instructions/*.instructions.md`) following the official "awesome-copilot" best practices.
9
+
10
+ ## When to Use
11
+
12
+ Use this skill when you want to:
13
+
14
+ - Create a reusable prompt or context for Copilot.
15
+ - Standardize code generation for a specific language, framework, or library.
16
+ - Define code review guidelines for specific file types.
17
+ - Ensure consistent documentation styles.
18
+
19
+ ## Workflow
20
+
21
+ 1. **Identify the Need**: Determine what domain or specific task you need instructions for (e.g., "React Components", "Python Testing", "API Documentation").
22
+ 2. **Define Scope**: Decide which files these instructions should apply to (e.g., `**/*.tsx`, `tests/*.py`).
23
+ 3. **Generate Content**: using the template below, the agent will help you draft the content.
24
+
25
+ ## Template
26
+
27
+ The generated file should look like this:
28
+
29
+ ```markdown
30
+ ---
31
+ description: 'Brief description of the instruction purpose and scope'
32
+ applyTo: 'glob pattern (e.g., **/*.ts)'
33
+ ---
34
+
35
+ # [Title: e.g., React Component Guidelines]
36
+
37
+ [Brief introduction explaining the purpose and scope]
38
+
39
+ ## General Instructions
40
+ [High-level guidelines and principles]
41
+
42
+ ## Best Practices
43
+ - [Be Specific]
44
+ - [Show Why]
45
+
46
+ ## Code Standards
47
+ [Naming conventions, formatting, style rules]
48
+
49
+ ## Examples
50
+
51
+ ### Good Example
52
+ \`\`\`language
53
+ // Recommended approach
54
+ code example here
55
+ \`\`\`
56
+
57
+ ### Bad Example
58
+ \`\`\`language
59
+ // Avoid this pattern
60
+ code example here
61
+ \`\`\`
62
+ ```
63
+
64
+ ## Tips for Success
65
+
66
+ - **Be Specific**: Copilot works best with concrete examples.
67
+ - **Use "applyTo" Correctly**: Ensure the glob pattern hits the right files.
68
+ - **Keep it Updated**: As your project evolves, update these instructions.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: make-prompt
3
+ description: Generates custom Copilot prompt files (.prompt.md) following official best practices. Use when you need to create structured, reusable prompts for complex tasks, code generation, or architectural planning.
4
+ ---
5
+
6
+ # Make Prompt
7
+
8
+ This skill helps you generate custom prompt files (`.prompt.md`) for GitHub Copilot following the official "awesome-copilot" best practices and the "Professional Prompt Builder" guide.
9
+
10
+ ## When to Use
11
+
12
+ Use this skill when you want to:
13
+
14
+ - Create structured, reusable prompts for complex tasks.
15
+ - Define a specific persona or expertise level for Copilot.
16
+ - Create blueprints, implementation plans, or specifications.
17
+ - Ensure consistent output formats for code generation or analysis.
18
+
19
+ ## Workflow
20
+
21
+ 1. **Discovery**: Identify the Identity, Persona, Task, Context, Instructions, Output, Tools, and Validation criteria.
22
+ 2. **Generate**: Create the `.prompt.md` file using the template below.
23
+
24
+ ## Template
25
+
26
+ The generated file should look like this:
27
+
28
+ ```markdown
29
+ ---
30
+ description: "[Clear, concise description from requirements]"
31
+ agent: "[agent|ask|edit based on task type]"
32
+ tools: ["[appropriate tools based on functionality]"]
33
+ model: "[only if specific model required]"
34
+ ---
35
+
36
+ # [Prompt Title]
37
+
38
+ [Persona definition - specific role and expertise]
39
+ Example: "You are a senior .NET architect with 10+ years of experience..."
40
+
41
+ ## [Task Section]
42
+ [Clear task description with specific requirements]
43
+
44
+ ## [Instructions Section]
45
+ [Step-by-step instructions following established patterns]
46
+
47
+ ## [Context/Input Section]
48
+ [Variable usage and context requirements]
49
+ Example: "Uses \${selection} and \${file}"
50
+
51
+ ## [Output Section]
52
+ [Expected output format and structure]
53
+
54
+ ## [Quality/Validation Section]
55
+ [Success criteria and validation steps]
56
+ ```
57
+
58
+ ## Checklist for Quality
59
+
60
+ - ✅ **Clear Structure**: Logical flow.
61
+ - ✅ **Specific Instructions**: Actionable directions.
62
+ - ✅ **Proper Context**: All necessary info is included.
63
+ - ✅ **Tool Integration**: Correct tools selected.
64
+ - ✅ **Error Handling**: Guidance for edge cases.
65
+
66
+ ## Tools Reference
67
+
68
+ - **File Operations**: `codebase`, `editFiles`, `search`, `problems`
69
+ - **Execution**: `runCommands`, `runTasks`, `runTests`, `terminalLastCommand`
70
+ - **External**: `fetch`, `githubRepo`, `openSimpleBrowser`
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: make-skill
3
+ description: 'Create new Agent Skills for GitHub Copilot from prompts or by duplicating this template. Use when asked to "create a skill", "make a new skill", "scaffold a skill", or when building specialized AI capabilities with bundled resources. Generates SKILL.md files with proper frontmatter, directory structure, and optional scripts/references/assets folders.'
4
+ ---
5
+
6
+ # Make Skill
7
+
8
+ A meta-skill for creating new Agent Skills. Use this skill when you need to scaffold a new skill folder, generate a SKILL.md file, or help users understand the Agent Skills specification.
9
+
10
+ ## When to Use This Skill
11
+
12
+ - User asks to "create a skill", "make a new skill", or "scaffold a skill"
13
+ - User wants to add a specialized capability to their GitHub Copilot setup
14
+ - User needs help structuring a skill with bundled resources
15
+ - User wants to duplicate this template as a starting point
16
+
17
+ ## Prerequisites
18
+
19
+ - Understanding of what the skill should accomplish
20
+ - A clear, keyword-rich description of capabilities and triggers
21
+ - Knowledge of any bundled resources needed (scripts, references, assets, templates)
22
+
23
+ ## Creating a New Skill
24
+
25
+ ### Step 1: Create the Skill Directory
26
+
27
+ Create a new folder with a lowercase, hyphenated name:
28
+
29
+ ```
30
+ skills/<skill-name>/
31
+ └── SKILL.md # Required
32
+ ```
33
+
34
+ ### Step 2: Generate SKILL.md with Frontmatter
35
+
36
+ Every skill requires YAML frontmatter with `name` and `description`:
37
+
38
+ ```yaml
39
+ ---
40
+ name: <skill-name>
41
+ description: '<What it does>. Use when <specific triggers, scenarios, keywords users might say>.'
42
+ ---
43
+ ```
44
+
45
+ #### Frontmatter Field Requirements
46
+
47
+ | Field | Required | Constraints |
48
+ |-------|----------|-------------|
49
+ | `name` | **Yes** | 1-64 chars, lowercase letters/numbers/hyphens only, must match folder name |
50
+ | `description` | **Yes** | 1-1024 chars, must describe WHAT it does AND WHEN to use it |
51
+ | `license` | No | License name or reference to bundled LICENSE.txt |
52
+ | `compatibility` | No | 1-500 chars, environment requirements if needed |
53
+ | `metadata` | No | Key-value pairs for additional properties |
54
+ | `allowed-tools` | No | Space-delimited list of pre-approved tools (experimental) |
55
+
56
+ #### Description Best Practices
57
+
58
+ **CRITICAL**: The `description` is the PRIMARY mechanism for automatic skill discovery. Include:
59
+
60
+ 1. **WHAT** the skill does (capabilities)
61
+ 2. **WHEN** to use it (triggers, scenarios, file types)
62
+ 3. **Keywords** users might mention in prompts
63
+
64
+ **Good example:**
65
+
66
+ ```yaml
67
+ description: 'Toolkit for testing local web applications using Playwright. Use when asked to verify frontend functionality, debug UI behavior, capture browser screenshots, or view browser console logs. Supports Chrome, Firefox, and WebKit.'
68
+ ```
69
+
70
+ **Poor example:**
71
+
72
+ ```yaml
73
+ description: 'Web testing helpers'
74
+ ```
75
+
76
+ ### Step 3: Write the Skill Body
77
+
78
+ After the frontmatter, add markdown instructions. Recommended sections:
79
+
80
+ | Section | Purpose |
81
+ |---------|---------|
82
+ | `# Title` | Brief overview |
83
+ | `## When to Use This Skill` | Reinforces description triggers |
84
+ | `## Prerequisites` | Required tools, dependencies |
85
+ | `## Step-by-Step Workflows` | Numbered steps for tasks |
86
+ | `## Troubleshooting` | Common issues and solutions |
87
+ | `## References` | Links to bundled docs |
88
+
89
+ ### Step 4: Add Optional Directories (If Needed)
90
+
91
+ | Folder | Purpose | When to Use |
92
+ |--------|---------|-------------|
93
+ | `scripts/` | Executable code (Python, Bash, JS) | Automation that performs operations |
94
+ | `references/` | Documentation agent reads | API references, schemas, guides |
95
+ | `assets/` | Static files used AS-IS | Images, fonts, templates |
96
+ | `templates/` | Starter code agent modifies | Scaffolds to extend |
97
+
98
+ ## Example: Complete Skill Structure
99
+
100
+ ```
101
+ my-awesome-skill/
102
+ ├── SKILL.md # Required instructions
103
+ ├── LICENSE.txt # Optional license file
104
+ ├── scripts/
105
+ │ └── helper.py # Executable automation
106
+ ├── references/
107
+ │ ├── api-reference.md # Detailed docs
108
+ │ └── examples.md # Usage examples
109
+ ├── assets/
110
+ │ └── diagram.png # Static resources
111
+ └── templates/
112
+ └── starter.ts # Code scaffold
113
+ ```
114
+
115
+ ## Quick Start: Duplicate This Template
116
+
117
+ 1. Copy the `make-skill-template/` folder
118
+ 2. Rename to your skill name (lowercase, hyphens)
119
+ 3. Update `SKILL.md`:
120
+ - Change `name:` to match folder name
121
+ - Write a keyword-rich `description:`
122
+ - Replace body content with your instructions
123
+ 4. Add bundled resources as needed
124
+ 5. Validate with `npm run skill:validate`
125
+
126
+ ## Validation Checklist
127
+
128
+ - [ ] Folder name is lowercase with hyphens
129
+ - [ ] `name` field matches folder name exactly
130
+ - [ ] `description` is 10-1024 characters
131
+ - [ ] `description` explains WHAT and WHEN
132
+ - [ ] `description` is wrapped in single quotes
133
+ - [ ] Body content is under 500 lines
134
+ - [ ] Bundled assets are under 5MB each
135
+
136
+ ## Troubleshooting
137
+
138
+ | Issue | Solution |
139
+ |-------|----------|
140
+ | Skill not discovered | Improve description with more keywords and triggers |
141
+ | Validation fails on name | Ensure lowercase, no consecutive hyphens, matches folder |
142
+ | Description too short | Add capabilities, triggers, and keywords |
143
+ | Assets not found | Use relative paths from skill root |
144
+
145
+ ## References
146
+
147
+ - Agent Skills official spec: <https://agentskills.io/specification>
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { cancel, confirm, intro, isCancel, multiselect, note, outro, spinner, } from "@clack/prompts";
5
5
  import fs from "fs-extra";
6
6
  import pc from "picocolors";
7
+ import { fetchSkillFromGitHub } from "../utils/github.js";
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  const PROJECT_ROOT = path.resolve(__dirname, "../..");
@@ -14,7 +15,7 @@ const PLATFORM_PATHS = {
14
15
  antigravity: path.join(os.homedir(), ".gemini/antigravity/global_skills"),
15
16
  gemini: path.join(os.homedir(), ".gemini/skills"),
16
17
  };
17
- const PLATFORM_OPTIONS = [
18
+ export const PLATFORM_OPTIONS = [
18
19
  {
19
20
  label: "Antigravity",
20
21
  value: "antigravity",
@@ -28,12 +29,11 @@ const PLATFORM_OPTIONS = [
28
29
  hint: "~/.codeium/windsurf/skills",
29
30
  },
30
31
  ];
31
- async function installSkill(skillName, platform, overwrite) {
32
- const sourcePath = path.join(SKILLS_DIR, skillName);
32
+ async function installSkill(skillName, platform, overwrite, sourcePath = path.join(SKILLS_DIR, skillName)) {
33
33
  const targetBaseDir = PLATFORM_PATHS[platform];
34
34
  const targetPath = path.join(targetBaseDir, skillName);
35
35
  if (!(await fs.pathExists(sourcePath))) {
36
- throw new Error(`Skill '${skillName}' not found`);
36
+ throw new Error(`Skill '${skillName}' not found at ${sourcePath}`);
37
37
  }
38
38
  if (!overwrite && (await fs.pathExists(targetPath))) {
39
39
  return false;
@@ -42,23 +42,53 @@ async function installSkill(skillName, platform, overwrite) {
42
42
  await fs.copy(sourcePath, targetPath, { overwrite });
43
43
  return true;
44
44
  }
45
- export async function addSkill() {
45
+ export async function addSkill(url) {
46
46
  console.clear();
47
47
  intro(pc.bgCyan(pc.black(" AI Skills Manager ")));
48
+ let tempDir = null;
49
+ let selectedSkills = [];
48
50
  try {
49
- // 1. Check Skills Directory
50
- if (!(await fs.pathExists(SKILLS_DIR))) {
51
- cancel(pc.red("Skills directory not found!"));
52
- process.exit(1);
51
+ // 1. Determine Source (Local vs GitHub)
52
+ if (url) {
53
+ const s = spinner();
54
+ s.start("Fetching skill from GitHub...");
55
+ try {
56
+ const result = await fetchSkillFromGitHub(url);
57
+ tempDir = result.tempDir;
58
+ selectedSkills = [result.skillName];
59
+ s.stop(pc.green(`Fetched skill: ${result.skillName}`));
60
+ }
61
+ catch (e) {
62
+ s.stop(pc.red("Failed to fetch skill"));
63
+ throw e;
64
+ }
53
65
  }
54
- const entries = await fs.readdir(SKILLS_DIR, { withFileTypes: true });
55
- const availableSkills = entries
56
- .filter((entry) => entry.isDirectory())
57
- .map((entry) => ({ label: entry.name, value: entry.name }))
58
- .sort((a, b) => a.label.localeCompare(b.label));
59
- if (availableSkills.length === 0) {
60
- cancel("No skills found in the skills directory.");
61
- process.exit(0);
66
+ else {
67
+ // Local Selection Logic
68
+ if (!(await fs.pathExists(SKILLS_DIR))) {
69
+ cancel(pc.red("Skills directory not found!"));
70
+ process.exit(1);
71
+ }
72
+ const entries = await fs.readdir(SKILLS_DIR, { withFileTypes: true });
73
+ const availableSkills = entries
74
+ .filter((entry) => entry.isDirectory())
75
+ .map((entry) => ({ label: entry.name, value: entry.name }))
76
+ .sort((a, b) => a.label.localeCompare(b.label));
77
+ if (availableSkills.length === 0) {
78
+ cancel("No skills found in the skills directory.");
79
+ process.exit(0);
80
+ }
81
+ // Select Skills
82
+ const skills = await multiselect({
83
+ message: "Select skills to install:",
84
+ options: availableSkills,
85
+ required: true,
86
+ });
87
+ if (isCancel(skills)) {
88
+ cancel("Operation cancelled.");
89
+ process.exit(0);
90
+ }
91
+ selectedSkills = skills;
62
92
  }
63
93
  // 2. Select Platforms
64
94
  const platforms = await multiselect({
@@ -67,22 +97,14 @@ export async function addSkill() {
67
97
  required: true,
68
98
  });
69
99
  if (isCancel(platforms)) {
70
- cancel("Operation cancelled.");
71
- process.exit(0);
72
- }
73
- // 3. Select Skills
74
- const skills = await multiselect({
75
- message: "Select skills to install:",
76
- options: availableSkills,
77
- required: true,
78
- });
79
- if (isCancel(skills)) {
100
+ console.log("Cleaning up...");
101
+ if (tempDir)
102
+ await fs.remove(tempDir);
80
103
  cancel("Operation cancelled.");
81
104
  process.exit(0);
82
105
  }
83
106
  const selectedPlatforms = platforms;
84
- const selectedSkills = skills;
85
- // 4. Check for existing skills
107
+ // 3. Check for existing skills
86
108
  const existingSkills = [];
87
109
  for (const platform of selectedPlatforms) {
88
110
  for (const skill of selectedSkills) {
@@ -109,14 +131,16 @@ export async function addSkill() {
109
131
  initialValue: false,
110
132
  });
111
133
  if (isCancel(shouldOverwrite)) {
134
+ if (tempDir)
135
+ await fs.remove(tempDir);
112
136
  cancel("Operation cancelled.");
113
137
  process.exit(0);
114
138
  }
115
139
  overwrite = shouldOverwrite;
116
140
  }
117
- // 5. Confirmation Note
141
+ // 4. Confirmation Note
118
142
  note(`Installing ${selectedSkills.length} skills to ${selectedPlatforms.length} platforms...`, "Summary");
119
- // 6. Installation Loop with Spinner
143
+ // 5. Installation Loop with Spinner
120
144
  const s = spinner();
121
145
  s.start("Installing skills...");
122
146
  const errors = [];
@@ -127,7 +151,14 @@ export async function addSkill() {
127
151
  const message = `Installing ${pc.bold(skill)} to ${pc.cyan(platform)}...`;
128
152
  s.message(message);
129
153
  try {
130
- const installed = await installSkill(skill, platform, overwrite);
154
+ // Use specific source path for each skill
155
+ // If tempDir is set, it means we have a single skill fetched from GitHub
156
+ // The sourceDir was set to the parent of tempDir, so joining sourceDir + skillName works
157
+ // However, for local skills, sourceDir is SKILLS_DIR
158
+ const currentSourcePath = url && tempDir
159
+ ? tempDir // Special handling for single downloaded skill
160
+ : path.join(SKILLS_DIR, skill);
161
+ const installed = await installSkill(skill, platform, overwrite, currentSourcePath);
131
162
  if (installed) {
132
163
  installedCount++;
133
164
  }
@@ -141,6 +172,10 @@ export async function addSkill() {
141
172
  }
142
173
  }
143
174
  }
175
+ // Cleanup
176
+ if (tempDir) {
177
+ await fs.remove(tempDir);
178
+ }
144
179
  if (errors.length > 0) {
145
180
  s.stop(pc.yellow(`Completed with errors. Installed: ${installedCount}, Skipped: ${skippedCount}, Errors: ${errors.length}`));
146
181
  console.error(pc.red("\nErrors encountered:"));
@@ -154,6 +189,8 @@ export async function addSkill() {
154
189
  outro("You're all set!");
155
190
  }
156
191
  catch (error) {
192
+ if (tempDir)
193
+ await fs.remove(tempDir);
157
194
  cancel(`An error occurred: ${error}`);
158
195
  process.exit(1);
159
196
  }
@@ -0,0 +1,66 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { cancel, confirm, intro, isCancel, outro, spinner, } from "@clack/prompts";
4
+ import fs from "fs-extra";
5
+ import pc from "picocolors";
6
+ import { fetchSkillFromGitHub } from "../utils/github.js";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const PROJECT_ROOT = path.resolve(__dirname, "../..");
10
+ const SKILLS_DIR = path.join(PROJECT_ROOT, "skills");
11
+ export async function importSkill(url) {
12
+ console.clear();
13
+ intro(pc.bgCyan(pc.black(" AI Skills Manager : Import ")));
14
+ if (!url) {
15
+ cancel("GitHub URL is required.");
16
+ process.exit(1);
17
+ }
18
+ let tempDir = null;
19
+ try {
20
+ // 1. Fetch Skill
21
+ const s = spinner();
22
+ s.start("Fetching skill from GitHub...");
23
+ let skillName = "";
24
+ try {
25
+ const result = await fetchSkillFromGitHub(url);
26
+ tempDir = result.tempDir;
27
+ skillName = result.skillName;
28
+ s.stop(pc.green(`Fetched skill: ${skillName}`));
29
+ }
30
+ catch (e) {
31
+ s.stop(pc.red("Failed to fetch skill"));
32
+ throw e;
33
+ }
34
+ // 2. Determine Target
35
+ const targetPath = path.join(SKILLS_DIR, skillName);
36
+ // 3. Check Existence & Confirm Overwrite
37
+ if (await fs.pathExists(targetPath)) {
38
+ const shouldOverwrite = await confirm({
39
+ message: `Skill '${skillName}' already exists in the repo. Overwrite?`,
40
+ initialValue: false,
41
+ });
42
+ if (isCancel(shouldOverwrite) || !shouldOverwrite) {
43
+ if (tempDir)
44
+ await fs.remove(tempDir);
45
+ cancel("Operation cancelled.");
46
+ process.exit(0);
47
+ }
48
+ }
49
+ // 4. Move/Copy to Skills Directory
50
+ s.start(`Importing ${skillName} to ${SKILLS_DIR}...`);
51
+ await fs.ensureDir(SKILLS_DIR);
52
+ await fs.copy(tempDir, targetPath, { overwrite: true });
53
+ s.stop(pc.green(`Successfully imported ${skillName}!`));
54
+ // Cleanup
55
+ if (tempDir) {
56
+ await fs.remove(tempDir);
57
+ }
58
+ outro(`Skill available at: ${targetPath}`);
59
+ }
60
+ catch (error) {
61
+ if (tempDir)
62
+ await fs.remove(tempDir);
63
+ cancel(`An error occurred: ${error}`);
64
+ process.exit(1);
65
+ }
66
+ }
@@ -0,0 +1,54 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
+ export async function fetchSkillFromGitHub(url) {
5
+ const regex = /github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/;
6
+ const match = url.match(regex);
7
+ if (!match) {
8
+ throw new Error("Invalid GitHub URL. Format: https://github.com/owner/repo/tree/branch/path/to/skill");
9
+ }
10
+ const [, owner, repo, ref, skillPath] = match;
11
+ const skillName = path.basename(skillPath);
12
+ // Initial API URL for the root of the skill
13
+ const initialApiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${skillPath}?ref=${ref}`;
14
+ const tempDir = path.join(os.tmpdir(), "ai-agents-install", skillName);
15
+ await fs.ensureDir(tempDir);
16
+ await fs.emptyDir(tempDir);
17
+ // Recursive function to download directory contents
18
+ async function downloadDirectory(apiUrl, localDir) {
19
+ const response = await fetch(apiUrl, {
20
+ headers: {
21
+ "User-Agent": "ai-agents-cli",
22
+ Accept: "application/vnd.github.v3+json",
23
+ },
24
+ });
25
+ if (!response.ok) {
26
+ throw new Error(`Failed to fetch from GitHub (${response.status}): ${response.statusText}`);
27
+ }
28
+ const data = (await response.json());
29
+ if (!Array.isArray(data)) {
30
+ // If it's not an array, it might be a single file if the URL pointed to a file,
31
+ // but we expect a directory here per the Contents API usage for directories.
32
+ throw new Error("Invalid response from GitHub API. Expected directory listing.");
33
+ }
34
+ for (const item of data) {
35
+ if (item.type === "file" && item.download_url) {
36
+ const fileContentResponse = await fetch(item.download_url);
37
+ if (!fileContentResponse.ok) {
38
+ console.warn(`Failed to download ${item.name}: ${fileContentResponse.statusText}`);
39
+ continue;
40
+ }
41
+ const content = await fileContentResponse.text();
42
+ await fs.writeFile(path.join(localDir, item.name), content);
43
+ }
44
+ else if (item.type === "dir") {
45
+ const newLocalDir = path.join(localDir, item.name);
46
+ await fs.ensureDir(newLocalDir);
47
+ // Recursively download subdirectory using its API URL
48
+ await downloadDirectory(item.url, newLocalDir);
49
+ }
50
+ }
51
+ }
52
+ await downloadDirectory(initialApiUrl, tempDir);
53
+ return { tempDir, skillName };
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurokeita/add-skill",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI to install AI agent skills to various platforms",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "dev": "tsx bin/skills.ts",
15
+ "update:readme": "tsx scripts/update-readme.ts",
15
16
  "build": "tsc && tsx scripts/copy-assets.ts",
16
17
  "prepublishOnly": "pnpm run build",
17
18
  "lint": "pnpm run lint:ts && pnpm run lint:md",