@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.
Files changed (3) hide show
  1. package/README.md +65 -0
  2. package/package.json +31 -0
  3. 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
+ });