mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +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:
@@ -1,6 +1,17 @@
|
||||
/**
|
||||
* Hook Command - CLI endpoint for Claude Code hooks
|
||||
* Provides simplified interface for hook operations, replacing complex bash/curl commands
|
||||
*
|
||||
* Subcommands:
|
||||
* parse-status - Parse CCW status.json
|
||||
* session-context - Progressive session context loading
|
||||
* session-end - Trigger background memory tasks
|
||||
* stop - Handle Stop hook events
|
||||
* keyword - Detect mode keywords
|
||||
* pre-compact - Handle PreCompact events
|
||||
* notify - Send notification to dashboard
|
||||
* project-state - Output project state summary
|
||||
* template - Manage and execute hook templates
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
@@ -37,6 +48,7 @@ interface HookData {
|
||||
|
||||
/**
|
||||
* Read JSON data from stdin (for Claude Code hooks)
|
||||
* Returns the raw string data
|
||||
*/
|
||||
async function readStdin(): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -715,6 +727,164 @@ async function notifyAction(options: HookOptions): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Template action - manage and execute hook templates
|
||||
*
|
||||
* Subcommands:
|
||||
* list - List all available templates
|
||||
* install <id> - Install a template to settings.json
|
||||
* exec <id> - Execute a template (for hooks)
|
||||
*/
|
||||
async function templateAction(subcommand: string, args: string[], options: HookOptions): Promise<void> {
|
||||
const { stdin } = options;
|
||||
|
||||
// Dynamic import to avoid circular dependencies
|
||||
const {
|
||||
getTemplate,
|
||||
getAllTemplates,
|
||||
listTemplatesByCategory,
|
||||
executeTemplate,
|
||||
installTemplateToSettings,
|
||||
type HookInputData,
|
||||
} = await import('../core/hooks/hook-templates.js');
|
||||
|
||||
switch (subcommand) {
|
||||
case 'list': {
|
||||
const byCategory = listTemplatesByCategory();
|
||||
const categoryNames: Record<string, string> = {
|
||||
notification: 'Notification',
|
||||
indexing: 'Indexing',
|
||||
automation: 'Automation',
|
||||
utility: 'Utility',
|
||||
protection: 'Protection',
|
||||
};
|
||||
|
||||
if (stdin) {
|
||||
// JSON output for programmatic use
|
||||
process.stdout.write(JSON.stringify(byCategory, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(chalk.green('Hook Templates'));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
|
||||
for (const [category, templates] of Object.entries(byCategory)) {
|
||||
if (templates.length === 0) continue;
|
||||
console.log(chalk.cyan(`\n${categoryNames[category] || category}:`));
|
||||
for (const t of templates) {
|
||||
console.log(` ${chalk.yellow(t.id)}`);
|
||||
console.log(` ${chalk.gray(t.description)}`);
|
||||
console.log(` Trigger: ${t.trigger}${t.matcher ? ` (${t.matcher})` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.gray('\n─'.repeat(50)));
|
||||
console.log(chalk.gray('Usage: ccw hook template install <id> [--scope project|global]'));
|
||||
console.log(chalk.gray(' ccw hook template exec <id> --stdin'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
case 'install': {
|
||||
const templateId = args[0];
|
||||
if (!templateId) {
|
||||
console.error(chalk.red('Error: template ID required'));
|
||||
console.error(chalk.gray('Usage: ccw hook template install <id> [--scope project|global]'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scope = args.includes('--global') ? 'global' : 'project';
|
||||
const result = installTemplateToSettings(templateId, scope);
|
||||
|
||||
if (stdin) {
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
process.exit(result.success ? 0 : 1);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
console.log(chalk.green(result.message));
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(chalk.red(result.message));
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'exec': {
|
||||
const templateId = args[0];
|
||||
if (!templateId) {
|
||||
if (stdin) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red('Error: template ID required'));
|
||||
console.error(chalk.gray('Usage: ccw hook template exec <id> --stdin'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const template = getTemplate(templateId);
|
||||
if (!template) {
|
||||
if (stdin) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red(`Template not found: ${templateId}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read hook data from stdin
|
||||
let hookData: HookInputData = {};
|
||||
if (stdin) {
|
||||
try {
|
||||
const stdinData = await readStdin();
|
||||
if (stdinData) {
|
||||
hookData = JSON.parse(stdinData) as HookInputData;
|
||||
}
|
||||
} catch {
|
||||
// Continue with empty data
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await executeTemplate(templateId, hookData);
|
||||
|
||||
// Handle JSON output for hook decisions
|
||||
if (output.jsonOutput) {
|
||||
process.stdout.write(JSON.stringify(output.jsonOutput));
|
||||
process.exit(output.exitCode || 0);
|
||||
}
|
||||
|
||||
// Handle stderr
|
||||
if (output.stderr) {
|
||||
process.stderr.write(output.stderr);
|
||||
}
|
||||
|
||||
// Handle stdout
|
||||
if (output.stdout) {
|
||||
process.stdout.write(output.stdout);
|
||||
}
|
||||
|
||||
process.exit(output.exitCode || 0);
|
||||
} catch (error) {
|
||||
if (stdin) {
|
||||
// Silent failure for hooks
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (stdin) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(chalk.red(`Unknown template subcommand: ${subcommand}`));
|
||||
console.error(chalk.gray('Usage: ccw hook template <list|install|exec> [args]'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Project state action - reads project-tech.json and specs
|
||||
* and outputs a concise summary for session context injection.
|
||||
@@ -858,6 +1028,7 @@ ${chalk.bold('SUBCOMMANDS')}
|
||||
pre-compact Handle PreCompact hook events (checkpoint creation)
|
||||
notify Send notification to ccw view dashboard
|
||||
project-state Output project guidelines and recent dev history summary
|
||||
template Manage and execute hook templates (list, install, exec)
|
||||
|
||||
${chalk.bold('OPTIONS')}
|
||||
--stdin Read input from stdin (for Claude Code hooks)
|
||||
@@ -866,6 +1037,11 @@ ${chalk.bold('OPTIONS')}
|
||||
--session-id Session ID (alternative to stdin)
|
||||
--prompt Current prompt text (alternative to stdin)
|
||||
|
||||
${chalk.bold('TEMPLATE SUBCOMMANDS')}
|
||||
template list List all available hook templates
|
||||
template install <id> [--global] Install a template to settings.json
|
||||
template exec <id> --stdin Execute a template (for Claude Code hooks)
|
||||
|
||||
${chalk.bold('EXAMPLES')}
|
||||
${chalk.gray('# Parse CCW status file:')}
|
||||
ccw hook parse-status --path .workflow/.ccw/ccw-123/status.json
|
||||
@@ -894,6 +1070,15 @@ ${chalk.bold('EXAMPLES')}
|
||||
${chalk.gray('# Project state summary (hook, reads cwd from stdin):')}
|
||||
ccw hook project-state --stdin
|
||||
|
||||
${chalk.gray('# List available templates:')}
|
||||
ccw hook template list
|
||||
|
||||
${chalk.gray('# Install a template:')}
|
||||
ccw hook template install block-sensitive-files
|
||||
|
||||
${chalk.gray('# Execute a template (for hooks):')}
|
||||
ccw hook template exec block-sensitive-files --stdin
|
||||
|
||||
${chalk.bold('HOOK CONFIGURATION')}
|
||||
${chalk.gray('Add to .claude/settings.json for Stop hook:')}
|
||||
{
|
||||
@@ -908,14 +1093,16 @@ ${chalk.bold('HOOK CONFIGURATION')}
|
||||
}
|
||||
}
|
||||
|
||||
${chalk.gray('Add to .claude/settings.json for status tracking:')}
|
||||
${chalk.gray('Add to .claude/settings.json using templates (recommended):')}
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [{
|
||||
"trigger": "PostToolUse",
|
||||
"matcher": "Write",
|
||||
"command": "bash",
|
||||
"args": ["-c", "INPUT=$(cat); FILE_PATH=$(echo \\"$INPUT\\" | jq -r \\".tool_input.file_path // empty\\"); [ -n \\"$FILE_PATH\\" ] && ccw hook parse-status --path \\"$FILE_PATH\\""]
|
||||
"PreToolUse": [{
|
||||
"_templateId": "block-sensitive-files",
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "ccw hook template exec block-sensitive-files --stdin"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -930,6 +1117,8 @@ export async function hookCommand(
|
||||
args: string | string[],
|
||||
options: HookOptions
|
||||
): Promise<void> {
|
||||
const argsArray = Array.isArray(args) ? args : [args];
|
||||
|
||||
switch (subcommand) {
|
||||
case 'parse-status':
|
||||
await parseStatusAction(options);
|
||||
@@ -957,6 +1146,10 @@ export async function hookCommand(
|
||||
case 'project-state':
|
||||
await projectStateAction(options);
|
||||
break;
|
||||
case 'template':
|
||||
// template has its own subcommands: list, install, exec
|
||||
await templateAction(argsArray[0] || 'list', argsArray.slice(1), options);
|
||||
break;
|
||||
case 'help':
|
||||
case undefined:
|
||||
showHelp();
|
||||
|
||||
Reference in New Issue
Block a user