mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-03 15:43:11 +08:00
feat(hooks): introduce hook templates management and execution
- Added a new command `ccw hook template` with subcommands for listing, installing, and executing templates. - Implemented backend support for managing hook templates, including API routes for fetching and installing templates. - Created a new file `hook-templates.ts` to define and manage hook templates, including their execution logic. - Added a migration script to convert old-style hooks to the new template-based approach. - Updated documentation to reflect new template commands and usage examples. - Enhanced error handling and output formatting for better user experience.
This commit is contained in:
227
ccw/src/scripts/migrate-hook-templates.ts
Normal file
227
ccw/src/scripts/migrate-hook-templates.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Migrate Hook Templates Script
|
||||
*
|
||||
* This script helps migrate hook templates from inline bash/node commands
|
||||
* to the new `ccw hook template exec` approach, which avoids Windows Git Bash
|
||||
* quote handling issues.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/migrate-hook-templates.ts [--dry-run] [--settings path]
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
interface OldHookEntry {
|
||||
matcher?: string;
|
||||
command?: string;
|
||||
hooks?: Array<{
|
||||
type?: string;
|
||||
command?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
hooks?: Record<string, OldHookEntry[]>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Command patterns that indicate old-style inline scripts
|
||||
const OLD_PATTERNS = [
|
||||
// Bash inline with jq
|
||||
/bash\s+-c.*jq/,
|
||||
// Node inline with complex scripts
|
||||
/node\s+-e.*child_process/,
|
||||
/node\s+-e.*spawnSync/,
|
||||
// Long inline commands
|
||||
/command.*node -e ".*\{.*\}.*"/,
|
||||
];
|
||||
|
||||
// Mapping from old patterns to new template IDs
|
||||
const MIGRATION_MAP: Record<string, string> = {
|
||||
// Danger protection patterns
|
||||
'danger-bash-confirm': 'danger-bash-confirm',
|
||||
'danger-file-protection': 'danger-file-protection',
|
||||
'danger-git-destructive': 'danger-git-destructive',
|
||||
'danger-network-confirm': 'danger-network-confirm',
|
||||
'danger-system-paths': 'danger-system-paths',
|
||||
'danger-permission-change': 'danger-permission-change',
|
||||
// Memory patterns
|
||||
'memory-update-queue': 'memory-auto-compress',
|
||||
'memory-v2-extract': 'memory-v2-extract',
|
||||
// Notification patterns
|
||||
'session-start-notify': 'session-start-notify',
|
||||
'stop-notify': 'stop-notify',
|
||||
'session-state-watch': 'session-state-watch',
|
||||
// Automation patterns
|
||||
'auto-format-on-write': 'auto-format-on-write',
|
||||
'auto-lint-on-write': 'auto-lint-on-write',
|
||||
'block-sensitive-files': 'block-sensitive-files',
|
||||
'git-auto-stage': 'git-auto-stage',
|
||||
// Utility patterns
|
||||
'memory-preview-extract': 'memory-preview-extract',
|
||||
'memory-status-check': 'memory-status-check',
|
||||
'post-edit-index': 'post-edit-index',
|
||||
};
|
||||
|
||||
function detectTemplateFromCommand(command: string): string | null {
|
||||
// Check for explicit template ID patterns
|
||||
for (const [pattern, templateId] of Object.entries(MIGRATION_MAP)) {
|
||||
if (command.includes(pattern)) {
|
||||
return templateId;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for jq usage in bash (indicates old-style danger detection)
|
||||
if (command.includes('jq -r') && command.includes('DANGEROUS_PATTERNS')) {
|
||||
return 'danger-bash-confirm';
|
||||
}
|
||||
|
||||
// Check for curl to localhost:3456 (dashboard notification)
|
||||
if (command.includes('localhost:3456/api/hook')) {
|
||||
if (command.includes('SESSION_CREATED')) return 'session-start-notify';
|
||||
if (command.includes('TASK_COMPLETED')) return 'stop-notify';
|
||||
if (command.includes('FILE_MODIFIED')) return 'post-edit-index';
|
||||
if (command.includes('SESSION_STATE_CHANGED')) return 'session-state-watch';
|
||||
}
|
||||
|
||||
// Check for prettier
|
||||
if (command.includes('prettier --write')) {
|
||||
return 'auto-format-on-write';
|
||||
}
|
||||
|
||||
// Check for eslint
|
||||
if (command.includes('eslint --fix')) {
|
||||
return 'auto-lint-on-write';
|
||||
}
|
||||
|
||||
// Check for git add
|
||||
if (command.includes('git add -u')) {
|
||||
return 'git-auto-stage';
|
||||
}
|
||||
|
||||
// Check for sensitive file patterns
|
||||
if (command.includes('.env') && command.includes('credential')) {
|
||||
return 'block-sensitive-files';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isOldStyleHook(entry: OldHookEntry): boolean {
|
||||
const command = entry.command || entry.hooks?.[0]?.command || '';
|
||||
return OLD_PATTERNS.some(pattern => pattern.test(command));
|
||||
}
|
||||
|
||||
function migrateHookEntry(entry: OldHookEntry, trigger: string): OldHookEntry {
|
||||
const command = entry.command || entry.hooks?.[0]?.command || '';
|
||||
const templateId = detectTemplateFromCommand(command);
|
||||
|
||||
if (!templateId) {
|
||||
console.log(` ⚠️ Could not auto-detect template for: ${command.substring(0, 50)}...`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
console.log(` ✓ Migrating to template: ${templateId}`);
|
||||
|
||||
return {
|
||||
_templateId: templateId,
|
||||
matcher: entry.matcher,
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: `ccw hook template exec ${templateId} --stdin`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function migrateSettings(settings: Settings, dryRun: boolean): Settings {
|
||||
const migrated = { ...settings };
|
||||
|
||||
if (!migrated.hooks) {
|
||||
return migrated;
|
||||
}
|
||||
|
||||
console.log('\n📋 Analyzing hooks...');
|
||||
|
||||
for (const [trigger, entries] of Object.entries(migrated.hooks)) {
|
||||
if (!Array.isArray(entries)) continue;
|
||||
|
||||
console.log(`\n${trigger}:`);
|
||||
const newEntries: OldHookEntry[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (isOldStyleHook(entry)) {
|
||||
console.log(` Found old-style hook with matcher: ${entry.matcher || '*'}`);
|
||||
const migratedEntry = migrateHookEntry(entry, trigger);
|
||||
newEntries.push(migratedEntry);
|
||||
} else {
|
||||
// Check if already using template approach
|
||||
if (entry.hooks?.[0]?.command?.includes('ccw hook template')) {
|
||||
console.log(` ✓ Already using template: ${entry._templateId || 'unknown'}`);
|
||||
}
|
||||
newEntries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
migrated.hooks[trigger] = newEntries;
|
||||
}
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
|
||||
let settingsPath: string;
|
||||
const settingsIndex = args.indexOf('--settings');
|
||||
if (settingsIndex >= 0 && args[settingsIndex + 1]) {
|
||||
settingsPath = args[settingsIndex + 1];
|
||||
} else {
|
||||
// Default to project settings
|
||||
settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
||||
}
|
||||
|
||||
console.log('🔧 Hook Template Migration Script');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`Settings file: ${settingsPath}`);
|
||||
console.log(`Mode: ${dryRun ? 'DRY RUN (no changes)' : 'LIVE (will modify)'}`);
|
||||
|
||||
if (!existsSync(settingsPath)) {
|
||||
console.error(`\n❌ Settings file not found: ${settingsPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let settings: Settings;
|
||||
try {
|
||||
const content = readFileSync(settingsPath, 'utf8');
|
||||
settings = JSON.parse(content);
|
||||
} catch (e) {
|
||||
console.error(`\n❌ Failed to parse settings: ${(e as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const migrated = migrateSettings(settings, dryRun);
|
||||
|
||||
if (dryRun) {
|
||||
console.log('\n📄 Migrated settings (dry run):');
|
||||
console.log(JSON.stringify(migrated, null, 2));
|
||||
} else {
|
||||
// Backup original
|
||||
const backupPath = `${settingsPath}.backup-${Date.now()}`;
|
||||
writeFileSync(backupPath, JSON.stringify(settings, null, 2));
|
||||
console.log(`\n💾 Backup saved to: ${backupPath}`);
|
||||
|
||||
// Write migrated
|
||||
writeFileSync(settingsPath, JSON.stringify(migrated, null, 2));
|
||||
console.log(`\n✅ Settings migrated successfully!`);
|
||||
}
|
||||
|
||||
console.log('\n📌 Next steps:');
|
||||
console.log(' 1. Review the migrated settings');
|
||||
console.log(' 2. Test your hooks to ensure they work correctly');
|
||||
console.log(' 3. Run "ccw hook template list" to see all available templates');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user