Read the script
#!/usr/bin/env npx tsx
/**
* Skill Version Hook v1.1.0
*
* SKILL.md 파일이 수정될 때 자동으로 버전 백업을 생성합니다.
*
* Hook Event: PostToolUse (Write, Edit tools)
*
* 동작:
* 1. Write/Edit 도구가 SKILL.md를 수정했는지 확인
* 2. SKILL.md에서 버전 정보 추출 (pre-release 지원: 1.0.0-alpha, 1.0.0-beta.1)
* 3. releases/ 폴더에 버전별 백업 생성
* 4. CHANGELOG.md 업데이트
* 5. Last Updated 날짜 자동 업데이트
* 6. 로그 출력
*
* 파일 명명 규칙: v{VERSION}_{YYYY-MM-DD}_SKILL.md
*
* Changelog:
* - v1.1.0: Pre-release 지원, Last Updated 자동화, 경로 버그 수정, 성능 최적화
* - v1.0.0: 초기 버전
*/
import * as fs from "fs";
import * as path from "path";
// ============================================================================
// Types
// ============================================================================
interface PostToolUseInput {
session_id: string;
tool_name: string;
tool_input: {
file_path?: string;
content?: string;
old_string?: string;
new_string?: string;
};
tool_output?: {
success?: boolean;
error?: string;
};
transcript_path?: string;
}
interface HookResult {
continue: boolean;
message?: string;
}
// ============================================================================
// Constants
// ============================================================================
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const LOG_DIR = path.join(PROJECT_DIR, ".claude", "hooks", "logs");
// ============================================================================
// Main Hook
// ============================================================================
async function main(): Promise<void> {
try {
// Read input from stdin
const inputData = fs.readFileSync(0, "utf-8");
const input: PostToolUseInput = JSON.parse(inputData);
const result = await processHook(input);
// Output result
console.log(JSON.stringify(result));
} catch (error: any) {
// Graceful degradation - don't block the session
console.log(
JSON.stringify({
continue: true,
message: `[skill-version-hook] Error: ${error.message}`,
})
);
}
}
async function processHook(input: PostToolUseInput): Promise<HookResult> {
const { tool_name, tool_input, tool_output } = input;
// 1. Check if this is a Write or Edit tool
if (tool_name !== "Write" && tool_name !== "Edit") {
return { continue: true };
}
// 2. Check if the tool succeeded
if (tool_output?.success === false) {
return { continue: true };
}
// 3. Check if the file is a SKILL.md
const filePath = tool_input?.file_path;
if (!filePath || !isSkillMdFile(filePath)) {
return { continue: true };
}
// 4. Extract skill name from path
const skillName = extractSkillName(filePath);
if (!skillName) {
return { continue: true };
}
// 5. Read the SKILL.md content once (optimization: single read)
let content: string;
try {
content = fs.readFileSync(filePath, "utf-8");
} catch (error: any) {
log(`[skill-version-hook] Failed to read ${filePath}: ${error.message}`);
return { continue: true };
}
// 6. Extract version from content (supports pre-release: 1.0.0-alpha, 1.0.0-beta.1)
const version = extractVersionFromContent(content);
if (!version) {
log(`[skill-version-hook] No version found in ${filePath}, skipping backup`);
return {
continue: true,
message: `[skill-version-hook] No version header found in ${skillName}/SKILL.md`,
};
}
// 7. Create releases directory if needed
const releasesDir = path.join(path.dirname(filePath), "releases");
if (!fs.existsSync(releasesDir)) {
fs.mkdirSync(releasesDir, { recursive: true });
}
// 8. Generate backup filename (sanitize pre-release for filename)
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const safeVersion = version.replace(/[^a-zA-Z0-9.-]/g, "-");
const backupFilename = `v${safeVersion}_${today}_SKILL.md`;
const backupPath = path.join(releasesDir, backupFilename);
// 9. Check if backup already exists for this version
if (fs.existsSync(backupPath)) {
log(`[skill-version-hook] Backup already exists: ${backupFilename}`);
return {
continue: true,
message: `[skill-version-hook] Backup already exists for ${skillName} v${version}`,
};
}
// 10. Update Last Updated date in SKILL.md
const updatedContent = updateLastUpdated(content, today);
if (updatedContent !== content) {
try {
fs.writeFileSync(filePath, updatedContent, "utf-8");
content = updatedContent;
log(`[skill-version-hook] Updated Last Updated to ${today}`);
} catch (error: any) {
log(`[skill-version-hook] Failed to update Last Updated: ${error.message}`);
}
}
// 11. Create backup
try {
fs.writeFileSync(backupPath, content, "utf-8");
log(`[skill-version-hook] Created backup: ${backupFilename}`);
// 12. Update CHANGELOG.md
const changelogPath = path.join(path.dirname(filePath), "CHANGELOG.md");
updateChangelog(changelogPath, skillName, version, today);
// 13. Add initial development notice for 0.x.x versions
const versionNote = version.startsWith("0.")
? " (Initial Development)"
: "";
return {
continue: true,
message: `[skill-version-hook] Backed up ${skillName}/SKILL.md to releases/${backupFilename}${versionNote}`,
};
} catch (error: any) {
log(`[skill-version-hook] Failed to create backup: ${error.message}`);
return {
continue: true,
message: `[skill-version-hook] Failed to backup: ${error.message}`,
};
}
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Check if the file path is a SKILL.md file
* Fixed: Use regex for cross-platform path separator compatibility
*/
function isSkillMdFile(filePath: string): boolean {
// Normalize path and convert to forward slashes for co