📦 deps(thirdparty): update snapshots
This commit is contained in:
+271
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Initialize a new Claude skill from template or scratch
|
||||
*
|
||||
* Usage:
|
||||
* bun run init-skill.ts <skill-name> <output-dir>
|
||||
* bun run init-skill.ts <skill-name> <output-dir> --template <template-name>
|
||||
*
|
||||
* Templates: api-wrapper, document-processor, dev-workflow, research-synthesizer, simple
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
||||
const TEMPLATES_DIR = path.join(SCRIPT_DIR, "../templates/skill-archetypes");
|
||||
|
||||
/**
|
||||
* Result of skill initialization.
|
||||
*/
|
||||
interface InitResult {
|
||||
/** Whether initialization succeeded or failed */
|
||||
status: "success" | "error";
|
||||
/** Path to created skill directory */
|
||||
skillDir?: string;
|
||||
/** Template used if any */
|
||||
template?: string;
|
||||
/** Files created during initialization */
|
||||
files?: string[];
|
||||
/** Suggested next steps after initialization */
|
||||
nextSteps?: string[];
|
||||
/** Error message if status is "error" */
|
||||
error?: string;
|
||||
/** Available templates when template not found */
|
||||
availableTemplates?: string[];
|
||||
}
|
||||
|
||||
function getAvailableTemplates(): string[] {
|
||||
if (!fs.existsSync(TEMPLATES_DIR)) return [];
|
||||
return fs
|
||||
.readdirSync(TEMPLATES_DIR)
|
||||
.filter((f) => fs.statSync(path.join(TEMPLATES_DIR, f)).isDirectory());
|
||||
}
|
||||
|
||||
function copyDir(src: string, dest: string): string[] {
|
||||
const copiedFiles: string[] = [];
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
// Prevent path traversal attacks
|
||||
if (entry.name.includes("..") || path.isAbsolute(entry.name)) {
|
||||
throw new Error(`Invalid file name: ${entry.name}`);
|
||||
}
|
||||
const srcPath = path.join(src, entry.name);
|
||||
// Rename SKILL.template.md to SKILL.md during copy
|
||||
const destName =
|
||||
entry.name === "SKILL.template.md" ? "SKILL.md" : entry.name;
|
||||
const destPath = path.join(dest, destName);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
copiedFiles.push(...copyDir(srcPath, destPath));
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
copiedFiles.push(path.relative(dest, destPath) || destName);
|
||||
}
|
||||
}
|
||||
|
||||
return copiedFiles;
|
||||
}
|
||||
|
||||
function toTitleCase(str: string): string {
|
||||
return str
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function createMinimalSkill(skillName: string, outputDir: string): InitResult {
|
||||
const skillDir = path.join(outputDir, skillName);
|
||||
|
||||
if (fs.existsSync(skillDir)) {
|
||||
return {
|
||||
status: "error",
|
||||
error: `Directory already exists: ${skillDir}`,
|
||||
};
|
||||
}
|
||||
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
|
||||
const titleName = toTitleCase(skillName);
|
||||
const skillMd = `---
|
||||
name: ${titleName}
|
||||
description: TODO - Describe what this skill does and when to use it. Include trigger keywords users might mention.
|
||||
---
|
||||
|
||||
# ${titleName}
|
||||
|
||||
## Quick Start
|
||||
|
||||
TODO: Fastest path to value (3-5 lines)
|
||||
|
||||
## Instructions
|
||||
|
||||
When this skill is activated:
|
||||
|
||||
1. **Step 1**
|
||||
- Detail
|
||||
|
||||
2. **Step 2**
|
||||
- Detail
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Basic Usage
|
||||
|
||||
\`\`\`
|
||||
TODO: Add concrete example
|
||||
\`\`\`
|
||||
|
||||
## Best Practices
|
||||
|
||||
- TODO: Add best practices
|
||||
|
||||
## Related Skills
|
||||
|
||||
- TODO: Add related skills if any
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(skillDir, "SKILL.md"), skillMd);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
skillDir,
|
||||
files: ["SKILL.md"],
|
||||
nextSteps: [
|
||||
"Edit SKILL.md frontmatter - description is critical for discovery",
|
||||
"Replace all TODO placeholders with actual content",
|
||||
"Add examples that demonstrate real usage",
|
||||
"Run validate-claude-skill to check quality",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function createFromTemplate(
|
||||
skillName: string,
|
||||
outputDir: string,
|
||||
templateName: string,
|
||||
): InitResult {
|
||||
const templateDir = path.join(TEMPLATES_DIR, templateName);
|
||||
const availableTemplates = getAvailableTemplates();
|
||||
|
||||
if (!fs.existsSync(templateDir)) {
|
||||
return {
|
||||
status: "error",
|
||||
error: `Template '${templateName}' not found`,
|
||||
availableTemplates,
|
||||
};
|
||||
}
|
||||
|
||||
const skillDir = path.join(outputDir, skillName);
|
||||
|
||||
if (fs.existsSync(skillDir)) {
|
||||
return {
|
||||
status: "error",
|
||||
error: `Directory already exists: ${skillDir}`,
|
||||
};
|
||||
}
|
||||
|
||||
const files = copyDir(templateDir, skillDir);
|
||||
|
||||
// Make scripts executable
|
||||
const scriptsDir = path.join(skillDir, "scripts");
|
||||
if (fs.existsSync(scriptsDir)) {
|
||||
for (const script of fs.readdirSync(scriptsDir)) {
|
||||
const scriptPath = path.join(scriptsDir, script);
|
||||
fs.chmodSync(scriptPath, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
skillDir,
|
||||
template: templateName,
|
||||
files,
|
||||
nextSteps: [
|
||||
"Replace all {{PLACEHOLDERS}} in SKILL.md with actual values",
|
||||
"Update scripts/ with your specific logic",
|
||||
"Craft a strong description for discoverability",
|
||||
"Test with real inputs",
|
||||
"Run validate-claude-skill to check quality",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function showUsage(): void {
|
||||
const templates = getAvailableTemplates();
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
usage: "init-skill.ts <skill-name> <output-dir> [--template <name>]",
|
||||
examples: [
|
||||
"bun run init-skill.ts my-skill ~/.claude/skills",
|
||||
"bun run init-skill.ts github-api .claude/skills --template api-wrapper",
|
||||
],
|
||||
availableTemplates: templates,
|
||||
templateDescriptions: {
|
||||
"api-wrapper": "For wrapping external APIs (REST, GraphQL)",
|
||||
"document-processor":
|
||||
"For working with file formats (PDF, DOCX, etc)",
|
||||
"dev-workflow": "For automating development tasks (git, CI, etc)",
|
||||
"research-synthesizer": "For gathering and synthesizing information",
|
||||
simple: "Minimal skill without scripts",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Main
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
||||
showUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const templateIdx = args.indexOf("--template");
|
||||
let template: string | null = null;
|
||||
|
||||
if (templateIdx !== -1) {
|
||||
template = args[templateIdx + 1];
|
||||
args.splice(templateIdx, 2);
|
||||
}
|
||||
|
||||
const [skillName, outputDir] = args;
|
||||
|
||||
if (!skillName || !outputDir) {
|
||||
showUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate skill name (minimum 2 chars, kebab-case)
|
||||
if (skillName.length < 2 || !/^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]{2}$/.test(skillName)) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
status: "error",
|
||||
error:
|
||||
"Skill name must be kebab-case (lowercase with hyphens, min 2 chars)",
|
||||
examples: ["my-skill", "github-api", "pdf-processor", "ai"],
|
||||
}),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Expand ~ to home directory
|
||||
const expandedOutputDir = outputDir.replace(/^~/, homedir());
|
||||
|
||||
let result: InitResult;
|
||||
|
||||
if (template) {
|
||||
result = createFromTemplate(skillName, expandedOutputDir, template);
|
||||
} else {
|
||||
result = createMinimalSkill(skillName, expandedOutputDir);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(result.status === "success" ? 0 : 1);
|
||||
Reference in New Issue
Block a user