@funstack/skill-installer 1.0.0-alpha.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 +65 -0
- package/package.json +31 -0
- package/src/index.ts +208 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @funstack/skill-installer
|
|
2
|
+
|
|
3
|
+
A CLI tool to install AI Agent skills by copying skill files to the appropriate location.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @funstack/skill-installer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Interactive Mode
|
|
14
|
+
|
|
15
|
+
Run the tool with the path to your skill directory:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
skill-installer ./path/to/my-skill
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
You'll see an interactive menu to select your AI agent:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Select AI Agent (↑↓ to move, Enter to confirm)
|
|
25
|
+
❯ 1. Claude Code (./.claude/skills)
|
|
26
|
+
2. Codex (./.codex/skills)
|
|
27
|
+
3. GitHub Copilot (./.github/skills)
|
|
28
|
+
4. Cursor (./.cursor/skills)
|
|
29
|
+
5. Gemini CLI (./.gemini/skills)
|
|
30
|
+
6. Windsurf (./.windsurf/skills)
|
|
31
|
+
7. OpenCode (./.opencode/skills)
|
|
32
|
+
8. Other (custom path)
|
|
33
|
+
|
|
34
|
+
Missing your agent? Let us know: https://github.com/uhyo/funstack-skill-installer/issues
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Use arrow keys (↑↓) to navigate
|
|
38
|
+
- Press a number key (1-9) to jump to an option
|
|
39
|
+
- Press Enter to confirm your selection
|
|
40
|
+
|
|
41
|
+
### Non-Interactive Mode
|
|
42
|
+
|
|
43
|
+
For CI/CD pipelines or scripted installations, set the `SKILL_INSTALL_PATH` environment variable:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
SKILL_INSTALL_PATH=./.claude/skills skill-installer ./path/to/my-skill
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Supported AI Agents
|
|
50
|
+
|
|
51
|
+
| Agent | Installation Path |
|
|
52
|
+
|-------|------------------|
|
|
53
|
+
| Claude Code | `./.claude/skills` |
|
|
54
|
+
| Codex | `./.codex/skills` |
|
|
55
|
+
| GitHub Copilot | `./.github/skills` |
|
|
56
|
+
| Cursor | `./.cursor/skills` |
|
|
57
|
+
| Gemini CLI | `./.gemini/skills` |
|
|
58
|
+
| Windsurf | `./.windsurf/skills` |
|
|
59
|
+
| OpenCode | `./.opencode/skills` |
|
|
60
|
+
|
|
61
|
+
Don't see your agent? [Open an issue](https://github.com/uhyo/funstack-skill-installer/issues) to request support!
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@funstack/skill-installer",
|
|
3
|
+
"version": "1.0.0-alpha.0",
|
|
4
|
+
"description": "CLI tool to install Agent Skills",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/uhyo/funstack-skill-installer.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "index.js",
|
|
11
|
+
"bin": {
|
|
12
|
+
"skill-installer": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "uhyo <uhyo@uhy.ooo>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^25.1.0",
|
|
19
|
+
"prettier": "^3.8.1",
|
|
20
|
+
"typescript": "^5.9.3"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"format": "prettier --write . --experimental-cli",
|
|
27
|
+
"format:check": "prettier --check . --experimental-cli",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as readline from "node:readline/promises";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { stdin, stdout } from "node:process";
|
|
7
|
+
import { styleText } from "node:util";
|
|
8
|
+
|
|
9
|
+
interface Option {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderOptions(options: Option[], selectedIndex: number): void {
|
|
15
|
+
options.forEach((option, index) => {
|
|
16
|
+
const isSelected = index === selectedIndex;
|
|
17
|
+
const number = styleText("dim", `${index + 1}.`);
|
|
18
|
+
const pathDisplay = styleText("dim", `(${option.path ?? "custom path"})`);
|
|
19
|
+
|
|
20
|
+
if (isSelected) {
|
|
21
|
+
const indicator = styleText("cyan", "❯");
|
|
22
|
+
const name = styleText(["bold", "cyan"], option.name);
|
|
23
|
+
console.log(`${indicator} ${number} ${name} ${pathDisplay}`);
|
|
24
|
+
} else {
|
|
25
|
+
console.log(` ${number} ${option.name} ${pathDisplay}`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clearOptions(count: number): void {
|
|
31
|
+
// Move cursor up and clear each line
|
|
32
|
+
for (let i = 0; i < count; i++) {
|
|
33
|
+
stdout.write("\x1b[1A"); // Move up one line
|
|
34
|
+
stdout.write("\x1b[2K"); // Clear the line
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function selectOption(
|
|
39
|
+
options: Option[],
|
|
40
|
+
footer?: string,
|
|
41
|
+
): Promise<number> {
|
|
42
|
+
let selectedIndex = 0;
|
|
43
|
+
|
|
44
|
+
const render = () => {
|
|
45
|
+
renderOptions(options, selectedIndex);
|
|
46
|
+
if (footer) {
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(styleText("dim", footer));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const clear = () => {
|
|
53
|
+
const lineCount = options.length + (footer ? 2 : 0);
|
|
54
|
+
clearOptions(lineCount);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Initial render
|
|
58
|
+
render();
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
stdin.setRawMode(true);
|
|
62
|
+
stdin.resume();
|
|
63
|
+
|
|
64
|
+
const onKeypress = (data: Buffer) => {
|
|
65
|
+
const key = data.toString();
|
|
66
|
+
|
|
67
|
+
// Handle arrow keys (escape sequences)
|
|
68
|
+
if (key === "\x1b[A") {
|
|
69
|
+
// Up arrow
|
|
70
|
+
clear();
|
|
71
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
72
|
+
render();
|
|
73
|
+
} else if (key === "\x1b[B") {
|
|
74
|
+
// Down arrow
|
|
75
|
+
clear();
|
|
76
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
77
|
+
render();
|
|
78
|
+
} else if (key >= "1" && key <= "9") {
|
|
79
|
+
// Number keys 1-9
|
|
80
|
+
const targetIndex = parseInt(key, 10) - 1;
|
|
81
|
+
if (targetIndex < options.length) {
|
|
82
|
+
clear();
|
|
83
|
+
selectedIndex = targetIndex;
|
|
84
|
+
render();
|
|
85
|
+
}
|
|
86
|
+
} else if (key === "\r" || key === "\n") {
|
|
87
|
+
// Enter key
|
|
88
|
+
stdin.setRawMode(false);
|
|
89
|
+
stdin.pause();
|
|
90
|
+
stdin.removeListener("data", onKeypress);
|
|
91
|
+
resolve(selectedIndex);
|
|
92
|
+
} else if (key === "\x03") {
|
|
93
|
+
// Ctrl+C
|
|
94
|
+
stdin.setRawMode(false);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
stdin.on("data", onKeypress);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
// 1. Parse CLI arguments
|
|
105
|
+
const skillPath = process.argv[2];
|
|
106
|
+
if (!skillPath) {
|
|
107
|
+
console.error("Usage: skill-installer <skill-path>");
|
|
108
|
+
console.error(" <skill-path> Path to the skill directory to install");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate that the skill path exists and is a directory
|
|
113
|
+
try {
|
|
114
|
+
const stats = await fs.stat(skillPath);
|
|
115
|
+
if (!stats.isDirectory()) {
|
|
116
|
+
console.error(`Error: ${skillPath} is not a directory`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
console.error(`Error: ${skillPath} does not exist`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. Determine installation path
|
|
125
|
+
let destinationPath: string;
|
|
126
|
+
|
|
127
|
+
if (!stdin.isTTY) {
|
|
128
|
+
// Non-TTY mode: read from environment variable
|
|
129
|
+
const envPath = process.env.SKILL_INSTALL_PATH;
|
|
130
|
+
if (!envPath) {
|
|
131
|
+
console.error(
|
|
132
|
+
"Error: stdin is not a TTY and SKILL_INSTALL_PATH is not set.",
|
|
133
|
+
);
|
|
134
|
+
console.error("");
|
|
135
|
+
console.error(
|
|
136
|
+
"In non-interactive mode, set the SKILL_INSTALL_PATH environment variable:",
|
|
137
|
+
);
|
|
138
|
+
console.error(
|
|
139
|
+
" SKILL_INSTALL_PATH=./.claude/skills skill-installer <skill-path>",
|
|
140
|
+
);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
destinationPath = envPath;
|
|
144
|
+
} else {
|
|
145
|
+
// TTY mode: interactive prompt
|
|
146
|
+
const options: Option[] = [
|
|
147
|
+
{ name: "Claude Code", path: "./.claude/skills" },
|
|
148
|
+
{ name: "Codex", path: "./.codex/skills" },
|
|
149
|
+
{ name: "GitHub Copilot", path: "./.github/skills" },
|
|
150
|
+
{ name: "Cursor", path: "./.cursor/skills" },
|
|
151
|
+
{ name: "Gemini CLI", path: "./.gemini/skills" },
|
|
152
|
+
{ name: "Windsurf", path: "./.windsurf/skills" },
|
|
153
|
+
{ name: "OpenCode", path: "./.opencode/skills" },
|
|
154
|
+
{ name: "Other", path: null },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
console.log(
|
|
158
|
+
"\n" +
|
|
159
|
+
styleText("bold", "Select AI Agent") +
|
|
160
|
+
styleText("dim", " (↑↓ to move, Enter to confirm)"),
|
|
161
|
+
);
|
|
162
|
+
const selectedIndex = await selectOption(
|
|
163
|
+
options,
|
|
164
|
+
"Missing your agent? Let us know: https://github.com/uhyo/funstack-skill-installer/issues",
|
|
165
|
+
);
|
|
166
|
+
const selectedOption = options[selectedIndex]!;
|
|
167
|
+
|
|
168
|
+
if (selectedOption.path !== null) {
|
|
169
|
+
destinationPath = selectedOption.path;
|
|
170
|
+
} else {
|
|
171
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
172
|
+
try {
|
|
173
|
+
const customPath = await rl.question(
|
|
174
|
+
"\nEnter custom installation path: ",
|
|
175
|
+
);
|
|
176
|
+
if (!customPath.trim()) {
|
|
177
|
+
console.error("Error: Installation path cannot be empty");
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
destinationPath = customPath.trim();
|
|
181
|
+
} finally {
|
|
182
|
+
rl.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 4. Copy skill files
|
|
188
|
+
// Create destination directory if it doesn't exist
|
|
189
|
+
await fs.mkdir(destinationPath, { recursive: true });
|
|
190
|
+
|
|
191
|
+
// Copy the skill directory contents to the destination
|
|
192
|
+
const skillName = path.basename(skillPath);
|
|
193
|
+
const finalDestination = path.join(destinationPath, skillName);
|
|
194
|
+
|
|
195
|
+
await fs.cp(skillPath, finalDestination, { recursive: true });
|
|
196
|
+
|
|
197
|
+
console.log(
|
|
198
|
+
"\n" +
|
|
199
|
+
styleText("green", "✓") +
|
|
200
|
+
" Skill installed successfully to: " +
|
|
201
|
+
styleText("bold", finalDestination),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
main().catch((error) => {
|
|
206
|
+
console.error("Error:", error.message);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
});
|