@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 +29 -0
- package/dist/bin/skills.js +7 -2
- package/dist/skills/make-instruction/SKILL.md +68 -0
- package/dist/skills/make-prompt/SKILL.md +70 -0
- package/dist/skills/make-skill/SKILL.md +147 -0
- package/dist/src/commands/add.js +69 -32
- package/dist/src/commands/import.js +66 -0
- package/dist/src/utils/github.js +54 -0
- package/package.json +2 -1
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:**
|
package/dist/bin/skills.js
CHANGED
|
@@ -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>
|
package/dist/src/commands/add.js
CHANGED
|
@@ -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.
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
141
|
+
// 4. Confirmation Note
|
|
118
142
|
note(`Installing ${selectedSkills.length} skills to ${selectedPlatforms.length} platforms...`, "Summary");
|
|
119
|
-
//
|
|
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
|
-
|
|
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.
|
|
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",
|