@kurokeita/add-skill 1.7.0 → 1.9.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.
@@ -12,8 +12,8 @@
12
12
  * node init_skill.cjs my-new-skill --path skills/public
13
13
  */
14
14
 
15
- const fs = require("node:fs");
16
- const path = require("node:path");
15
+ const fs = require("node:fs")
16
+ const path = require("node:path")
17
17
 
18
18
  const SKILL_TEMPLATE = `---
19
19
  name: {skill_name}
@@ -100,7 +100,7 @@ Files not intended to be loaded into context, but rather used within the output
100
100
  ---
101
101
 
102
102
  **Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
103
- `;
103
+ `
104
104
 
105
105
  const EXAMPLE_SCRIPT = `#!/usr/bin/env node
106
106
 
@@ -135,7 +135,7 @@ async function main() {
135
135
  }
136
136
 
137
137
  main();
138
- `;
138
+ `
139
139
 
140
140
  const EXAMPLE_REFERENCE = `# Reference Documentation for {skill_title}
141
141
 
@@ -154,24 +154,24 @@ Replace with actual reference content or delete if not needed.
154
154
  - Prerequisites
155
155
  - Step-by-step instructions
156
156
  - Best practices
157
- `;
157
+ `
158
158
 
159
159
  function titleCase(name) {
160
160
  return name
161
161
  .split("-")
162
162
  .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
163
- .join(" ");
163
+ .join(" ")
164
164
  }
165
165
 
166
166
  async function main() {
167
- const args = process.argv.slice(2);
167
+ const args = process.argv.slice(2)
168
168
  if (args.length < 3 || args[1] !== "--path") {
169
- console.log("Usage: node init_skill.cjs <skill-name> --path <path>");
170
- process.exit(1);
169
+ console.log("Usage: node init_skill.cjs <skill-name> --path <path>")
170
+ process.exit(1)
171
171
  }
172
172
 
173
- const skillName = args[0];
174
- const basePath = path.resolve(args[2]);
173
+ const skillName = args[0]
174
+ const basePath = path.resolve(args[2])
175
175
 
176
176
  // Prevent path traversal
177
177
  if (
@@ -179,30 +179,30 @@ async function main() {
179
179
  skillName.includes("/") ||
180
180
  skillName.includes("\\")
181
181
  ) {
182
- console.error("❌ Error: Skill name cannot contain path separators.");
183
- process.exit(1);
182
+ console.error("❌ Error: Skill name cannot contain path separators.")
183
+ process.exit(1)
184
184
  }
185
185
 
186
- const skillDir = path.join(basePath, skillName);
186
+ const skillDir = path.join(basePath, skillName)
187
187
 
188
188
  // Additional check to ensure the resolved skillDir is actually inside basePath
189
189
  if (!skillDir.startsWith(basePath)) {
190
- console.error("❌ Error: Invalid skill name or path.");
191
- process.exit(1);
190
+ console.error("❌ Error: Invalid skill name or path.")
191
+ process.exit(1)
192
192
  }
193
193
 
194
194
  if (fs.existsSync(skillDir)) {
195
- console.error(`❌ Error: Skill directory already exists: ${skillDir}`);
196
- process.exit(1);
195
+ console.error(`❌ Error: Skill directory already exists: ${skillDir}`)
196
+ process.exit(1)
197
197
  }
198
198
 
199
- const skillTitle = titleCase(skillName);
199
+ const skillTitle = titleCase(skillName)
200
200
 
201
201
  try {
202
- fs.mkdirSync(skillDir, { recursive: true });
203
- fs.mkdirSync(path.join(skillDir, "scripts"));
204
- fs.mkdirSync(path.join(skillDir, "references"));
205
- fs.mkdirSync(path.join(skillDir, "assets"));
202
+ fs.mkdirSync(skillDir, { recursive: true })
203
+ fs.mkdirSync(path.join(skillDir, "scripts"))
204
+ fs.mkdirSync(path.join(skillDir, "references"))
205
+ fs.mkdirSync(path.join(skillDir, "assets"))
206
206
 
207
207
  fs.writeFileSync(
208
208
  path.join(skillDir, "SKILL.md"),
@@ -210,26 +210,26 @@ async function main() {
210
210
  /{skill_title}/g,
211
211
  skillTitle,
212
212
  ),
213
- );
213
+ )
214
214
  fs.writeFileSync(
215
215
  path.join(skillDir, "scripts/example_script.cjs"),
216
216
  EXAMPLE_SCRIPT.replace(/{skill_name}/g, skillName),
217
217
  { mode: 0o755 },
218
- );
218
+ )
219
219
  fs.writeFileSync(
220
220
  path.join(skillDir, "references/example_reference.md"),
221
221
  EXAMPLE_REFERENCE.replace(/{skill_title}/g, skillTitle),
222
- );
222
+ )
223
223
  fs.writeFileSync(
224
224
  path.join(skillDir, "assets/example_asset.txt"),
225
225
  "Placeholder for assets.",
226
- );
226
+ )
227
227
 
228
- console.log(`✅ Skill '${skillName}' initialized at ${skillDir}`);
228
+ console.log(`✅ Skill '${skillName}' initialized at ${skillDir}`)
229
229
  } catch (err) {
230
- console.error(`❌ Error: ${err.message}`);
231
- process.exit(1);
230
+ console.error(`❌ Error: ${err.message}`)
231
+ process.exit(1)
232
232
  }
233
233
  }
234
234
 
235
- main();
235
+ main()
@@ -9,48 +9,48 @@
9
9
  * node package_skill.js <path/to/skill-folder> [output-directory]
10
10
  */
11
11
 
12
- const path = require("node:path");
13
- const { spawnSync } = require("node:child_process");
14
- const { validateSkill } = require("./validate_skill.cjs");
12
+ const path = require("node:path")
13
+ const { spawnSync } = require("node:child_process")
14
+ const { validateSkill } = require("./validate_skill.cjs")
15
15
 
16
16
  async function main() {
17
- const args = process.argv.slice(2);
17
+ const args = process.argv.slice(2)
18
18
  if (args.length < 1) {
19
19
  console.log(
20
20
  "Usage: node package_skill.js <path/to/skill-folder> [output-directory]",
21
- );
22
- process.exit(1);
21
+ )
22
+ process.exit(1)
23
23
  }
24
24
 
25
- const skillPathArg = args[0];
26
- const outputDirArg = args[1];
25
+ const skillPathArg = args[0]
26
+ const outputDirArg = args[1]
27
27
 
28
28
  if (skillPathArg.includes("..") || outputDirArg?.includes("..")) {
29
- console.error("❌ Error: Path traversal detected in arguments.");
30
- process.exit(1);
29
+ console.error("❌ Error: Path traversal detected in arguments.")
30
+ process.exit(1)
31
31
  }
32
32
 
33
- const skillPath = path.resolve(skillPathArg);
34
- const outputDir = outputDirArg ? path.resolve(outputDirArg) : process.cwd();
35
- const skillName = path.basename(skillPath);
33
+ const skillPath = path.resolve(skillPathArg)
34
+ const outputDir = outputDirArg ? path.resolve(outputDirArg) : process.cwd()
35
+ const skillName = path.basename(skillPath)
36
36
 
37
37
  // 1. Validate first
38
- console.log("🔍 Validating skill...");
39
- const result = validateSkill(skillPath);
38
+ console.log("🔍 Validating skill...")
39
+ const result = validateSkill(skillPath)
40
40
  if (!result.valid) {
41
- console.error(`❌ Validation failed: ${result.message}`);
42
- process.exit(1);
41
+ console.error(`❌ Validation failed: ${result.message}`)
42
+ process.exit(1)
43
43
  }
44
44
 
45
45
  if (result.warning) {
46
- console.warn(`⚠️ ${result.warning}`);
47
- console.log("Please resolve all TODOs before packaging.");
48
- process.exit(1);
46
+ console.warn(`⚠️ ${result.warning}`)
47
+ console.log("Please resolve all TODOs before packaging.")
48
+ process.exit(1)
49
49
  }
50
- console.log("✅ Skill is valid!");
50
+ console.log("✅ Skill is valid!")
51
51
 
52
52
  // 2. Package
53
- const outputFilename = path.join(outputDir, `${skillName}.skill`);
53
+ const outputFilename = path.join(outputDir, `${skillName}.skill`)
54
54
 
55
55
  try {
56
56
  // Zip everything except junk, keeping the folder structure
@@ -64,21 +64,21 @@ async function main() {
64
64
  const zipProcess = spawnSync("zip", ["-r", outputFilename, "."], {
65
65
  cwd: skillPath,
66
66
  stdio: "inherit",
67
- });
67
+ })
68
68
 
69
69
  if (zipProcess.error) {
70
- throw zipProcess.error;
70
+ throw zipProcess.error
71
71
  }
72
72
 
73
73
  if (zipProcess.status !== 0) {
74
- throw new Error(`zip command failed with exit code ${zipProcess.status}`);
74
+ throw new Error(`zip command failed with exit code ${zipProcess.status}`)
75
75
  }
76
76
 
77
- console.log(`✅ Successfully packaged skill to: ${outputFilename}`);
77
+ console.log(`✅ Successfully packaged skill to: ${outputFilename}`)
78
78
  } catch (err) {
79
- console.error(`❌ Error packaging: ${err.message}`);
80
- process.exit(1);
79
+ console.error(`❌ Error packaging: ${err.message}`)
80
+ process.exit(1)
81
81
  }
82
82
  }
83
83
 
84
- main();
84
+ main()
@@ -5,123 +5,123 @@
5
5
  * Leveraging existing dependencies when possible or providing a zero-dep fallback.
6
6
  */
7
7
 
8
- const fs = require("node:fs");
9
- const path = require("node:path");
8
+ const fs = require("node:fs")
9
+ const path = require("node:path")
10
10
 
11
11
  function validateSkill(skillPath) {
12
12
  if (!fs.existsSync(skillPath) || !fs.statSync(skillPath).isDirectory()) {
13
- return { valid: false, message: `Path is not a directory: ${skillPath}` };
13
+ return { valid: false, message: `Path is not a directory: ${skillPath}` }
14
14
  }
15
15
 
16
- const skillMdPath = path.join(skillPath, "SKILL.md");
16
+ const skillMdPath = path.join(skillPath, "SKILL.md")
17
17
  if (!fs.existsSync(skillMdPath)) {
18
- return { valid: false, message: "SKILL.md not found" };
18
+ return { valid: false, message: "SKILL.md not found" }
19
19
  }
20
20
 
21
- const content = fs.readFileSync(skillMdPath, "utf8");
21
+ const content = fs.readFileSync(skillMdPath, "utf8")
22
22
  if (!content.startsWith("---")) {
23
- return { valid: false, message: "No YAML frontmatter found" };
23
+ return { valid: false, message: "No YAML frontmatter found" }
24
24
  }
25
25
 
26
- const parts = content.split("---");
26
+ const parts = content.split("---")
27
27
  if (parts.length < 3) {
28
- return { valid: false, message: "Invalid frontmatter format" };
28
+ return { valid: false, message: "Invalid frontmatter format" }
29
29
  }
30
30
 
31
- const frontmatterText = parts[1];
31
+ const frontmatterText = parts[1]
32
32
 
33
- const nameMatch = frontmatterText.match(/^name:\s*(.+)$/m);
33
+ const nameMatch = frontmatterText.match(/^name:\s*(.+)$/m)
34
34
  // Match description: "text" or description: 'text' or description: text
35
35
  const descMatch = frontmatterText.match(
36
36
  /^description:\s*(?:'([^']*)'|"([^"]*)"|(.+))$/m,
37
- );
37
+ )
38
38
 
39
39
  if (!nameMatch)
40
- return { valid: false, message: 'Missing "name" in frontmatter' };
40
+ return { valid: false, message: 'Missing "name" in frontmatter' }
41
41
  if (!descMatch)
42
42
  return {
43
43
  valid: false,
44
44
  message: "Description must be a single-line string: description: ...",
45
- };
45
+ }
46
46
 
47
- const name = nameMatch[1].trim();
47
+ const name = nameMatch[1].trim()
48
48
  const description = (
49
49
  descMatch[1] !== undefined
50
50
  ? descMatch[1]
51
51
  : descMatch[2] !== undefined
52
52
  ? descMatch[2]
53
53
  : descMatch[3] || ""
54
- ).trim();
54
+ ).trim()
55
55
 
56
56
  if (description.includes("\n")) {
57
57
  return {
58
58
  valid: false,
59
59
  message: "Description must be a single line (no newlines)",
60
- };
60
+ }
61
61
  }
62
62
 
63
63
  if (!/^[a-z0-9-]+$/.test(name)) {
64
- return { valid: false, message: `Name "${name}" should be hyphen-case` };
64
+ return { valid: false, message: `Name "${name}" should be hyphen-case` }
65
65
  }
66
66
 
67
67
  if (description.length > 1024) {
68
- return { valid: false, message: "Description is too long (max 1024)" };
68
+ return { valid: false, message: "Description is too long (max 1024)" }
69
69
  }
70
70
 
71
71
  // Check for TODOs
72
- const files = getAllFiles(skillPath);
72
+ const files = getAllFiles(skillPath)
73
73
  for (const file of files) {
74
- const fileContent = fs.readFileSync(file, "utf8");
74
+ const fileContent = fs.readFileSync(file, "utf8")
75
75
  if (fileContent.includes("TODO:")) {
76
76
  return {
77
77
  valid: true,
78
78
  message: "Skill has unresolved TODOs",
79
79
  warning: `Found unresolved TODO in ${path.relative(skillPath, file)}`,
80
- };
80
+ }
81
81
  }
82
82
  }
83
83
 
84
- return { valid: true, message: "Skill is valid!" };
84
+ return { valid: true, message: "Skill is valid!" }
85
85
  }
86
86
 
87
87
  function getAllFiles(dir, fileList = []) {
88
- const files = fs.readdirSync(dir);
88
+ const files = fs.readdirSync(dir)
89
89
  files.forEach((file) => {
90
- const name = path.join(dir, file);
90
+ const name = path.join(dir, file)
91
91
  if (fs.statSync(name).isDirectory()) {
92
92
  if (!["node_modules", ".git", "__pycache__"].includes(file)) {
93
- getAllFiles(name, fileList);
93
+ getAllFiles(name, fileList)
94
94
  }
95
95
  } else {
96
- fileList.push(name);
96
+ fileList.push(name)
97
97
  }
98
- });
99
- return fileList;
98
+ })
99
+ return fileList
100
100
  }
101
101
 
102
102
  if (require.main === module) {
103
- const args = process.argv.slice(2);
103
+ const args = process.argv.slice(2)
104
104
  if (args.length !== 1) {
105
- console.log("Usage: node validate_skill.js <skill_directory>");
106
- process.exit(1);
105
+ console.log("Usage: node validate_skill.js <skill_directory>")
106
+ process.exit(1)
107
107
  }
108
108
 
109
- const skillDirArg = args[0];
109
+ const skillDirArg = args[0]
110
110
  if (skillDirArg.includes("..")) {
111
- console.error("❌ Error: Path traversal detected in skill directory path.");
112
- process.exit(1);
111
+ console.error("❌ Error: Path traversal detected in skill directory path.")
112
+ process.exit(1)
113
113
  }
114
114
 
115
- const result = validateSkill(path.resolve(skillDirArg));
115
+ const result = validateSkill(path.resolve(skillDirArg))
116
116
  if (result.warning) {
117
- console.warn(`⚠️ ${result.warning}`);
117
+ console.warn(`⚠️ ${result.warning}`)
118
118
  }
119
119
  if (result.valid) {
120
- console.log(`✅ ${result.message}`);
120
+ console.log(`✅ ${result.message}`)
121
121
  } else {
122
- console.error(`❌ ${result.message}`);
123
- process.exit(1);
122
+ console.error(`❌ ${result.message}`)
123
+ process.exit(1)
124
124
  }
125
125
  }
126
126
 
127
- module.exports = { validateSkill };
127
+ module.exports = { validateSkill }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurokeita/add-skill",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "CLI to install AI agent skills to various platforms",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,10 +13,12 @@
13
13
  "scripts": {
14
14
  "dev": "tsx bin/cli.ts",
15
15
  "update:readme": "tsx scripts/update-readme.ts",
16
- "build": "tsc && tsx scripts/copy-assets.ts",
16
+ "build": "tsc --noEmit && esbuild bin/cli.ts --bundle --platform=node --format=esm --outfile=dist/bin/cli.js --packages=external && tsx scripts/copy-assets.ts",
17
17
  "prepublishOnly": "pnpm run build",
18
+ "test": "vitest run",
19
+ "test:coverage": "vitest run --coverage",
18
20
  "lint": "pnpm run lint:ts && pnpm run lint:md '**/*.md'",
19
- "lint:ts": "biome check .",
21
+ "lint:ts": "biome check --error-on-warnings .",
20
22
  "lint:md": "markdownlint --ignore node_modules '**/*.md'",
21
23
  "format": "biome check --write . && pnpm run lint:md --fix",
22
24
  "release": "semantic-release"
@@ -38,17 +40,22 @@
38
40
  "@clack/prompts": "^0.11.0",
39
41
  "commander": "^14.0.2",
40
42
  "fs-extra": "^11.3.3",
43
+ "js-yaml": "^4.1.1",
41
44
  "picocolors": "^1.1.1"
42
45
  },
43
46
  "devDependencies": {
44
47
  "@biomejs/biome": "^2.3.11",
45
48
  "@semantic-release/git": "^10.0.1",
46
49
  "@types/fs-extra": "^11.0.4",
50
+ "@types/js-yaml": "^4.0.9",
47
51
  "@types/node": "^25.0.9",
52
+ "@vitest/coverage-v8": "^4.0.18",
53
+ "esbuild": "^0.25.0",
48
54
  "markdownlint-cli": "^0.47.0",
49
55
  "semantic-release": "^25.0.2",
50
56
  "tsx": "^4.21.0",
51
- "typescript": "^5.9.3"
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.0.18"
52
59
  },
53
60
  "publishConfig": {
54
61
  "access": "public"