mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-03 15:43:11 +08:00
feat: enhance hook templates management by adding protection category and improving command safety
This commit is contained in:
@@ -274,7 +274,8 @@ function getCategoryName(category: TemplateCategory, formatMessage: ReturnType<t
|
|||||||
notification: formatMessage({ id: 'cliHooks.templates.categories.notification' }),
|
notification: formatMessage({ id: 'cliHooks.templates.categories.notification' }),
|
||||||
indexing: formatMessage({ id: 'cliHooks.templates.categories.indexing' }),
|
indexing: formatMessage({ id: 'cliHooks.templates.categories.indexing' }),
|
||||||
automation: formatMessage({ id: 'cliHooks.templates.categories.automation' }),
|
automation: formatMessage({ id: 'cliHooks.templates.categories.automation' }),
|
||||||
utility: formatMessage({ id: 'cliHooks.templates.categories.utility' })
|
utility: formatMessage({ id: 'cliHooks.templates.categories.utility' }),
|
||||||
|
protection: formatMessage({ id: 'cliHooks.templates.categories.protection' }),
|
||||||
};
|
};
|
||||||
return names[category];
|
return names[category];
|
||||||
}
|
}
|
||||||
@@ -304,7 +305,7 @@ export function HookQuickTemplates({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Define category order
|
// Define category order
|
||||||
const categoryOrder: TemplateCategory[] = ['notification', 'indexing', 'automation'];
|
const categoryOrder: TemplateCategory[] = ['notification', 'indexing', 'automation', 'protection', 'utility'];
|
||||||
|
|
||||||
const handleInstall = async (templateId: string) => {
|
const handleInstall = async (templateId: string) => {
|
||||||
await onInstallTemplate(templateId);
|
await onInstallTemplate(templateId);
|
||||||
|
|||||||
@@ -79,12 +79,6 @@ interface SkillContextConfig {
|
|||||||
// All templates use `ccw hook template exec <id> --stdin` format
|
// All templates use `ccw hook template exec <id> --stdin` format
|
||||||
// This avoids Windows Git Bash quote handling issues
|
// This avoids Windows Git Bash quote handling issues
|
||||||
|
|
||||||
interface HookTemplate {
|
|
||||||
event: string;
|
|
||||||
matcher: string;
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template IDs that map to backend templates
|
// Template IDs that map to backend templates
|
||||||
const TEMPLATE_IDS = {
|
const TEMPLATE_IDS = {
|
||||||
'memory-update-queue': 'memory-auto-compress',
|
'memory-update-queue': 'memory-auto-compress',
|
||||||
@@ -106,72 +100,6 @@ const DANGER_OPTIONS = [
|
|||||||
{ id: 'permission-change', templateId: 'danger-permission-change', labelKey: 'cliHooks.wizards.dangerProtection.options.permissionChange', descKey: 'cliHooks.wizards.dangerProtection.options.permissionChangeDesc' },
|
{ id: 'permission-change', templateId: 'danger-permission-change', labelKey: 'cliHooks.wizards.dangerProtection.options.permissionChange', descKey: 'cliHooks.wizards.dangerProtection.options.permissionChangeDesc' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// ========== convertToClaudeCodeFormat (ported from old hook-manager.js) ==========
|
|
||||||
|
|
||||||
function convertToClaudeCodeFormat(hookData: {
|
|
||||||
command: string;
|
|
||||||
args: string[];
|
|
||||||
matcher?: string;
|
|
||||||
timeout?: number;
|
|
||||||
}): Record<string, unknown> {
|
|
||||||
let commandStr = hookData.command || '';
|
|
||||||
|
|
||||||
if (hookData.args && Array.isArray(hookData.args)) {
|
|
||||||
if (commandStr === 'bash' && hookData.args.length >= 2 && hookData.args[0] === '-c') {
|
|
||||||
const script = hookData.args[1];
|
|
||||||
const escapedScript = script.replace(/'/g, "'\\''");
|
|
||||||
commandStr = `bash -c '${escapedScript}'`;
|
|
||||||
if (hookData.args.length > 2) {
|
|
||||||
const additionalArgs = hookData.args.slice(2).map(arg =>
|
|
||||||
arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")
|
|
||||||
? `"${arg.replace(/"/g, '\\"')}"`
|
|
||||||
: arg
|
|
||||||
);
|
|
||||||
commandStr += ' ' + additionalArgs.join(' ');
|
|
||||||
}
|
|
||||||
} else if (commandStr === 'node' && hookData.args.length >= 2 && hookData.args[0] === '-e') {
|
|
||||||
const script = hookData.args[1];
|
|
||||||
const isWindows = typeof navigator !== 'undefined' && navigator.userAgent.includes('Win');
|
|
||||||
if (isWindows) {
|
|
||||||
const escapedScript = script.replace(/"/g, '\\"');
|
|
||||||
commandStr = `node -e "${escapedScript}"`;
|
|
||||||
} else {
|
|
||||||
const escapedScript = script.replace(/'/g, "'\\''");
|
|
||||||
commandStr = `node -e '${escapedScript}'`;
|
|
||||||
}
|
|
||||||
if (hookData.args.length > 2) {
|
|
||||||
const additionalArgs = hookData.args.slice(2).map(arg =>
|
|
||||||
arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")
|
|
||||||
? `"${arg.replace(/"/g, '\\"')}"`
|
|
||||||
: arg
|
|
||||||
);
|
|
||||||
commandStr += ' ' + additionalArgs.join(' ');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const quotedArgs = hookData.args.map(arg =>
|
|
||||||
arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")
|
|
||||||
? `"${arg.replace(/"/g, '\\"')}"`
|
|
||||||
: arg
|
|
||||||
);
|
|
||||||
commandStr = `${commandStr} ${quotedArgs.join(' ')}`.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const converted: Record<string, unknown> = {
|
|
||||||
hooks: [{
|
|
||||||
type: 'command',
|
|
||||||
command: commandStr,
|
|
||||||
...(hookData.timeout ? { timeout: Math.ceil(hookData.timeout / 1000) } : {}),
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (hookData.matcher) {
|
|
||||||
converted.matcher = hookData.matcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
return converted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Wizard Definitions ==========
|
// ========== Wizard Definitions ==========
|
||||||
|
|
||||||
const WIZARD_METADATA = {
|
const WIZARD_METADATA = {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { Card } from '@/components/ui/Card';
|
|||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { HookCard, HookFormDialog, HookQuickTemplates, HookWizard, type HookCardData, type HookFormData, type HookTriggerType, HOOK_TEMPLATES, type WizardType } from '@/components/hook';
|
import { HookCard, HookFormDialog, HookQuickTemplates, HookWizard, type HookCardData, type HookFormData, type HookTriggerType, HOOK_TEMPLATES, type WizardType } from '@/components/hook';
|
||||||
import { useHooks, useToggleHook } from '@/hooks';
|
import { useHooks, useToggleHook } from '@/hooks';
|
||||||
import { installHookTemplate } from '@/lib/api';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// ========== Types ==========
|
// ========== Types ==========
|
||||||
|
|||||||
@@ -745,8 +745,8 @@ async function templateAction(subcommand: string, args: string[], options: HookO
|
|||||||
listTemplatesByCategory,
|
listTemplatesByCategory,
|
||||||
executeTemplate,
|
executeTemplate,
|
||||||
installTemplateToSettings,
|
installTemplateToSettings,
|
||||||
type HookInputData,
|
|
||||||
} = await import('../core/hooks/hook-templates.js');
|
} = await import('../core/hooks/hook-templates.js');
|
||||||
|
type HookInputData = import('../core/hooks/hook-templates.js').HookInputData;
|
||||||
|
|
||||||
switch (subcommand) {
|
switch (subcommand) {
|
||||||
case 'list': {
|
case 'list': {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
import { spawnSync } from 'child_process';
|
import { spawnSync } from 'child_process';
|
||||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join, resolve, basename } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -79,22 +79,24 @@ export interface HookOutput {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send notification to dashboard via HTTP
|
* Send notification to dashboard via HTTP (using native fetch)
|
||||||
*/
|
*/
|
||||||
function notifyDashboard(type: string, payload: Record<string, unknown>): void {
|
function notifyDashboard(type: string, payload: Record<string, unknown>): void {
|
||||||
const data = JSON.stringify({
|
const data = {
|
||||||
type,
|
type,
|
||||||
...payload,
|
...payload,
|
||||||
project: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
|
project: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
};
|
||||||
|
|
||||||
spawnSync('curl', [
|
// Use native fetch (Node.js 18+) to avoid shell command injection
|
||||||
'-s', '-X', 'POST',
|
fetch('http://localhost:3456/api/hook', {
|
||||||
'-H', 'Content-Type: application/json',
|
method: 'POST',
|
||||||
'-d', data,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'http://localhost:3456/api/hook'
|
body: JSON.stringify(data),
|
||||||
], { stdio: 'inherit', shell: true });
|
}).catch(() => {
|
||||||
|
// Silently ignore errors - dashboard may not be running
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,6 +146,80 @@ function isDangerousGitCommand(cmd: string): boolean {
|
|||||||
return patterns.some(p => p.test(cmd));
|
return patterns.some(p => p.test(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely extract string value from unknown input
|
||||||
|
*/
|
||||||
|
function getStringInput(value: unknown): string {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate file path to prevent command injection
|
||||||
|
* Returns null if path is invalid, otherwise returns the sanitized path
|
||||||
|
*/
|
||||||
|
function validateFilePath(filePath: string): string | null {
|
||||||
|
if (!filePath || typeof filePath !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dangerous characters that could be used for command injection
|
||||||
|
const dangerousPatterns = /[;&|`$(){}[\]<>!\\]/;
|
||||||
|
if (dangerousPatterns.test(filePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path traversal attempts
|
||||||
|
if (filePath.includes('..')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for null bytes
|
||||||
|
if (filePath.includes('\0')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe spawnSync wrapper that avoids shell: true to prevent command injection
|
||||||
|
* On Windows, this uses .cmd extension for npm/npx commands
|
||||||
|
*/
|
||||||
|
function safeSpawnSync(command: string, args: string[]): { stdout: string; stderr: string; status: number | null } {
|
||||||
|
// Use spawnSync without shell to avoid command injection
|
||||||
|
// Note: On Windows, npx/npm/git may need .cmd extension
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
const execCommand = isWindows && !command.endsWith('.cmd') && ['npx', 'npm', 'git', 'ccw'].includes(command)
|
||||||
|
? `${command}.cmd`
|
||||||
|
: command;
|
||||||
|
|
||||||
|
return spawnSync(execCommand, args, {
|
||||||
|
stdio: ['inherit', 'pipe', 'pipe'],
|
||||||
|
shell: false,
|
||||||
|
encoding: 'utf8',
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe spawnSync with inherited stdio (for tools that need interactive output)
|
||||||
|
*/
|
||||||
|
function safeSpawnSyncInherit(command: string, args: string[]): void {
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
const execCommand = isWindows && !command.endsWith('.cmd') && ['npx', 'npm', 'git', 'ccw'].includes(command)
|
||||||
|
? `${command}.cmd`
|
||||||
|
: command;
|
||||||
|
|
||||||
|
spawnSync(execCommand, args, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: false,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if file is in protected system paths
|
* Check if file is in protected system paths
|
||||||
*/
|
*/
|
||||||
@@ -187,8 +263,8 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
trigger: 'PostToolUse',
|
trigger: 'PostToolUse',
|
||||||
matcher: 'Write|Edit',
|
matcher: 'Write|Edit',
|
||||||
execute: (data) => {
|
execute: (data) => {
|
||||||
const file = (data.tool_input?.file_path as string) || '';
|
const file = getStringInput(data.tool_input?.file_path);
|
||||||
if (/workflow-session\.json$|session-metadata\.json$/.test(file)) {
|
if (file && /workflow-session\.json$|session-metadata\.json$/.test(file)) {
|
||||||
try {
|
try {
|
||||||
if (existsSync(file)) {
|
if (existsSync(file)) {
|
||||||
const content = readFileSync(file, 'utf8');
|
const content = readFileSync(file, 'utf8');
|
||||||
@@ -239,9 +315,10 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
trigger: 'PostToolUse',
|
trigger: 'PostToolUse',
|
||||||
matcher: 'Write|Edit',
|
matcher: 'Write|Edit',
|
||||||
execute: (data) => {
|
execute: (data) => {
|
||||||
const file = (data.tool_input?.file_path as string) || '';
|
const rawFile = getStringInput(data.tool_input?.file_path);
|
||||||
|
const file = validateFilePath(rawFile);
|
||||||
if (file) {
|
if (file) {
|
||||||
spawnSync('npx', ['prettier', '--write', file], { stdio: 'inherit', shell: true });
|
safeSpawnSyncInherit('npx', ['prettier', '--write', file]);
|
||||||
}
|
}
|
||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
}
|
}
|
||||||
@@ -254,9 +331,10 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
trigger: 'PostToolUse',
|
trigger: 'PostToolUse',
|
||||||
matcher: 'Write|Edit',
|
matcher: 'Write|Edit',
|
||||||
execute: (data) => {
|
execute: (data) => {
|
||||||
const file = (data.tool_input?.file_path as string) || '';
|
const rawFile = getStringInput(data.tool_input?.file_path);
|
||||||
|
const file = validateFilePath(rawFile);
|
||||||
if (file) {
|
if (file) {
|
||||||
spawnSync('npx', ['eslint', '--fix', file], { stdio: 'inherit', shell: true });
|
safeSpawnSyncInherit('npx', ['eslint', '--fix', file]);
|
||||||
}
|
}
|
||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
}
|
}
|
||||||
@@ -268,7 +346,7 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
category: 'automation',
|
category: 'automation',
|
||||||
trigger: 'Stop',
|
trigger: 'Stop',
|
||||||
execute: () => {
|
execute: () => {
|
||||||
spawnSync('git', ['add', '-u'], { stdio: 'inherit', shell: true });
|
safeSpawnSyncInherit('git', ['add', '-u']);
|
||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -282,8 +360,8 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
trigger: 'PreToolUse',
|
trigger: 'PreToolUse',
|
||||||
matcher: 'Write|Edit',
|
matcher: 'Write|Edit',
|
||||||
execute: (data) => {
|
execute: (data) => {
|
||||||
const file = (data.tool_input?.file_path as string) || '';
|
const file = getStringInput(data.tool_input?.file_path);
|
||||||
if (isSensitiveFile(file)) {
|
if (file && isSensitiveFile(file)) {
|
||||||
return {
|
return {
|
||||||
exitCode: 2,
|
exitCode: 2,
|
||||||
stderr: `Blocked: modifying sensitive file ${file}`,
|
stderr: `Blocked: modifying sensitive file ${file}`,
|
||||||
@@ -324,9 +402,9 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
trigger: 'PreToolUse',
|
trigger: 'PreToolUse',
|
||||||
matcher: 'Write|Edit',
|
matcher: 'Write|Edit',
|
||||||
execute: (data) => {
|
execute: (data) => {
|
||||||
const file = (data.tool_input?.file_path as string) || '';
|
const file = getStringInput(data.tool_input?.file_path);
|
||||||
const protectedPatterns = /\.env|\.git\/|package-lock\.json|yarn\.lock|\.credentials|secrets|id_rsa|\.pem$|\.key$/i;
|
const protectedPatterns = /\.env|\.git\/|package-lock\.json|yarn\.lock|\.credentials|secrets|id_rsa|\.pem$|\.key$/i;
|
||||||
if (protectedPatterns.test(file)) {
|
if (file && protectedPatterns.test(file)) {
|
||||||
return {
|
return {
|
||||||
exitCode: 2,
|
exitCode: 2,
|
||||||
jsonOutput: {
|
jsonOutput: {
|
||||||
@@ -431,8 +509,8 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const file = (data.tool_input?.file_path as string) || '';
|
const file = getStringInput(data.tool_input?.file_path);
|
||||||
if (isSystemPath(file)) {
|
if (file && isSystemPath(file)) {
|
||||||
return {
|
return {
|
||||||
exitCode: 2,
|
exitCode: 2,
|
||||||
jsonOutput: {
|
jsonOutput: {
|
||||||
@@ -483,7 +561,7 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
trigger: 'PostToolUse',
|
trigger: 'PostToolUse',
|
||||||
matcher: 'Write|Edit',
|
matcher: 'Write|Edit',
|
||||||
execute: (data) => {
|
execute: (data) => {
|
||||||
const file = (data.tool_input?.file_path as string) || '';
|
const file = getStringInput(data.tool_input?.file_path);
|
||||||
if (file) {
|
if (file) {
|
||||||
notifyDashboard('FILE_MODIFIED', { file });
|
notifyDashboard('FILE_MODIFIED', { file });
|
||||||
}
|
}
|
||||||
@@ -512,7 +590,7 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
category: 'utility',
|
category: 'utility',
|
||||||
trigger: 'Stop',
|
trigger: 'Stop',
|
||||||
execute: () => {
|
execute: () => {
|
||||||
spawnSync('ccw', ['memory', 'consolidate', '--threshold', '50'], { stdio: 'inherit', shell: true });
|
safeSpawnSyncInherit('ccw', ['memory', 'consolidate', '--threshold', '50']);
|
||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -523,7 +601,7 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
category: 'utility',
|
category: 'utility',
|
||||||
trigger: 'SessionStart',
|
trigger: 'SessionStart',
|
||||||
execute: () => {
|
execute: () => {
|
||||||
spawnSync('ccw', ['memory', 'preview', '--include-native'], { stdio: 'inherit', shell: true });
|
safeSpawnSyncInherit('ccw', ['memory', 'preview', '--include-native']);
|
||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -534,7 +612,7 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
category: 'utility',
|
category: 'utility',
|
||||||
trigger: 'SessionStart',
|
trigger: 'SessionStart',
|
||||||
execute: () => {
|
execute: () => {
|
||||||
spawnSync('ccw', ['memory', 'status'], { stdio: 'inherit', shell: true });
|
safeSpawnSyncInherit('ccw', ['memory', 'status']);
|
||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -545,7 +623,7 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
category: 'utility',
|
category: 'utility',
|
||||||
trigger: 'Stop',
|
trigger: 'Stop',
|
||||||
execute: () => {
|
execute: () => {
|
||||||
spawnSync('ccw', ['core-memory', 'extract', '--max-sessions', '10'], { stdio: 'inherit', shell: true });
|
safeSpawnSyncInherit('ccw', ['core-memory', 'extract', '--max-sessions', '10']);
|
||||||
return { exitCode: 0 };
|
return { exitCode: 0 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -556,14 +634,11 @@ export const HOOK_TEMPLATES: HookTemplate[] = [
|
|||||||
category: 'utility',
|
category: 'utility',
|
||||||
trigger: 'Stop',
|
trigger: 'Stop',
|
||||||
execute: () => {
|
execute: () => {
|
||||||
const result = spawnSync('ccw', ['core-memory', 'extract', '--json'], {
|
const result = safeSpawnSync('ccw', ['core-memory', 'extract', '--json']);
|
||||||
encoding: 'utf8',
|
|
||||||
shell: true
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
const d = JSON.parse(result.stdout);
|
const d = JSON.parse(result.stdout);
|
||||||
if (d && d.total_stage1 >= 5) {
|
if (d && d.total_stage1 >= 5) {
|
||||||
spawnSync('ccw', ['core-memory', 'consolidate'], { stdio: 'inherit', shell: true });
|
safeSpawnSyncInherit('ccw', ['core-memory', 'consolidate']);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore parse errors
|
// Ignore parse errors
|
||||||
@@ -681,8 +756,8 @@ export function installTemplateToSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if already installed
|
// Check if already installed
|
||||||
const triggerHooks = hooks[template.trigger];
|
const triggerHooks = hooks[template.trigger] as Array<Record<string, unknown>>;
|
||||||
const alreadyInstalled = triggerHooks.some((h: Record<string, unknown>) =>
|
const alreadyInstalled = triggerHooks.some((h) =>
|
||||||
h._templateId === templateId
|
h._templateId === templateId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { join } from 'path';
|
|||||||
interface OldHookEntry {
|
interface OldHookEntry {
|
||||||
matcher?: string;
|
matcher?: string;
|
||||||
command?: string;
|
command?: string;
|
||||||
|
_templateId?: string;
|
||||||
hooks?: Array<{
|
hooks?: Array<{
|
||||||
type?: string;
|
type?: string;
|
||||||
command?: string;
|
command?: string;
|
||||||
@@ -29,56 +30,45 @@ interface Settings {
|
|||||||
|
|
||||||
// Command patterns that indicate old-style inline scripts
|
// Command patterns that indicate old-style inline scripts
|
||||||
const OLD_PATTERNS = [
|
const OLD_PATTERNS = [
|
||||||
// Bash inline with jq
|
|
||||||
/bash\s+-c.*jq/,
|
/bash\s+-c.*jq/,
|
||||||
// Node inline with complex scripts
|
|
||||||
/node\s+-e.*child_process/,
|
/node\s+-e.*child_process/,
|
||||||
/node\s+-e.*spawnSync/,
|
/node\s+-e.*spawnSync/,
|
||||||
// Long inline commands
|
|
||||||
/command.*node -e ".*\{.*\}.*"/,
|
/command.*node -e ".*\{.*\}.*"/,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mapping from old patterns to new template IDs
|
// Mapping from old patterns to new template IDs
|
||||||
const MIGRATION_MAP: Record<string, string> = {
|
const MIGRATION_MAP: Record<string, string> = {
|
||||||
// Danger protection patterns
|
|
||||||
'danger-bash-confirm': 'danger-bash-confirm',
|
'danger-bash-confirm': 'danger-bash-confirm',
|
||||||
'danger-file-protection': 'danger-file-protection',
|
'danger-file-protection': 'danger-file-protection',
|
||||||
'danger-git-destructive': 'danger-git-destructive',
|
'danger-git-destructive': 'danger-git-destructive',
|
||||||
'danger-network-confirm': 'danger-network-confirm',
|
'danger-network-confirm': 'danger-network-confirm',
|
||||||
'danger-system-paths': 'danger-system-paths',
|
'danger-system-paths': 'danger-system-paths',
|
||||||
'danger-permission-change': 'danger-permission-change',
|
'danger-permission-change': 'danger-permission-change',
|
||||||
// Memory patterns
|
|
||||||
'memory-update-queue': 'memory-auto-compress',
|
'memory-update-queue': 'memory-auto-compress',
|
||||||
'memory-v2-extract': 'memory-v2-extract',
|
'memory-v2-extract': 'memory-v2-extract',
|
||||||
// Notification patterns
|
|
||||||
'session-start-notify': 'session-start-notify',
|
'session-start-notify': 'session-start-notify',
|
||||||
'stop-notify': 'stop-notify',
|
'stop-notify': 'stop-notify',
|
||||||
'session-state-watch': 'session-state-watch',
|
'session-state-watch': 'session-state-watch',
|
||||||
// Automation patterns
|
|
||||||
'auto-format-on-write': 'auto-format-on-write',
|
'auto-format-on-write': 'auto-format-on-write',
|
||||||
'auto-lint-on-write': 'auto-lint-on-write',
|
'auto-lint-on-write': 'auto-lint-on-write',
|
||||||
'block-sensitive-files': 'block-sensitive-files',
|
'block-sensitive-files': 'block-sensitive-files',
|
||||||
'git-auto-stage': 'git-auto-stage',
|
'git-auto-stage': 'git-auto-stage',
|
||||||
// Utility patterns
|
'post-edit-index': 'post-edit-index',
|
||||||
'memory-preview-extract': 'memory-preview-extract',
|
'memory-preview-extract': 'memory-preview-extract',
|
||||||
'memory-status-check': 'memory-status-check',
|
'memory-status-check': 'memory-status-check',
|
||||||
'post-edit-index': 'post-edit-index',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function detectTemplateFromCommand(command: string): string | null {
|
function detectTemplateFromCommand(command: string): string | null {
|
||||||
// Check for explicit template ID patterns
|
|
||||||
for (const [pattern, templateId] of Object.entries(MIGRATION_MAP)) {
|
for (const [pattern, templateId] of Object.entries(MIGRATION_MAP)) {
|
||||||
if (command.includes(pattern)) {
|
if (command.includes(pattern)) {
|
||||||
return templateId;
|
return templateId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for jq usage in bash (indicates old-style danger detection)
|
|
||||||
if (command.includes('jq -r') && command.includes('DANGEROUS_PATTERNS')) {
|
if (command.includes('jq -r') && command.includes('DANGEROUS_PATTERNS')) {
|
||||||
return 'danger-bash-confirm';
|
return 'danger-bash-confirm';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for curl to localhost:3456 (dashboard notification)
|
|
||||||
if (command.includes('localhost:3456/api/hook')) {
|
if (command.includes('localhost:3456/api/hook')) {
|
||||||
if (command.includes('SESSION_CREATED')) return 'session-start-notify';
|
if (command.includes('SESSION_CREATED')) return 'session-start-notify';
|
||||||
if (command.includes('TASK_COMPLETED')) return 'stop-notify';
|
if (command.includes('TASK_COMPLETED')) return 'stop-notify';
|
||||||
@@ -86,22 +76,18 @@ function detectTemplateFromCommand(command: string): string | null {
|
|||||||
if (command.includes('SESSION_STATE_CHANGED')) return 'session-state-watch';
|
if (command.includes('SESSION_STATE_CHANGED')) return 'session-state-watch';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for prettier
|
|
||||||
if (command.includes('prettier --write')) {
|
if (command.includes('prettier --write')) {
|
||||||
return 'auto-format-on-write';
|
return 'auto-format-on-write';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for eslint
|
|
||||||
if (command.includes('eslint --fix')) {
|
if (command.includes('eslint --fix')) {
|
||||||
return 'auto-lint-on-write';
|
return 'auto-lint-on-write';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for git add
|
|
||||||
if (command.includes('git add -u')) {
|
if (command.includes('git add -u')) {
|
||||||
return 'git-auto-stage';
|
return 'git-auto-stage';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for sensitive file patterns
|
|
||||||
if (command.includes('.env') && command.includes('credential')) {
|
if (command.includes('.env') && command.includes('credential')) {
|
||||||
return 'block-sensitive-files';
|
return 'block-sensitive-files';
|
||||||
}
|
}
|
||||||
@@ -156,9 +142,8 @@ function migrateSettings(settings: Settings, dryRun: boolean): Settings {
|
|||||||
const migratedEntry = migrateHookEntry(entry, trigger);
|
const migratedEntry = migrateHookEntry(entry, trigger);
|
||||||
newEntries.push(migratedEntry);
|
newEntries.push(migratedEntry);
|
||||||
} else {
|
} else {
|
||||||
// Check if already using template approach
|
|
||||||
if (entry.hooks?.[0]?.command?.includes('ccw hook template')) {
|
if (entry.hooks?.[0]?.command?.includes('ccw hook template')) {
|
||||||
console.log(` ✓ Already using template: ${entry._templateId || 'unknown'}`);
|
console.log(` ✓ Already using template: ${(entry as OldHookEntry & { _templateId?: string })._templateId || 'unknown'}`);
|
||||||
}
|
}
|
||||||
newEntries.push(entry);
|
newEntries.push(entry);
|
||||||
}
|
}
|
||||||
@@ -179,7 +164,6 @@ async function main(): Promise<void> {
|
|||||||
if (settingsIndex >= 0 && args[settingsIndex + 1]) {
|
if (settingsIndex >= 0 && args[settingsIndex + 1]) {
|
||||||
settingsPath = args[settingsIndex + 1];
|
settingsPath = args[settingsIndex + 1];
|
||||||
} else {
|
} else {
|
||||||
// Default to project settings
|
|
||||||
settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,12 +192,10 @@ async function main(): Promise<void> {
|
|||||||
console.log('\n📄 Migrated settings (dry run):');
|
console.log('\n📄 Migrated settings (dry run):');
|
||||||
console.log(JSON.stringify(migrated, null, 2));
|
console.log(JSON.stringify(migrated, null, 2));
|
||||||
} else {
|
} else {
|
||||||
// Backup original
|
|
||||||
const backupPath = `${settingsPath}.backup-${Date.now()}`;
|
const backupPath = `${settingsPath}.backup-${Date.now()}`;
|
||||||
writeFileSync(backupPath, JSON.stringify(settings, null, 2));
|
writeFileSync(backupPath, JSON.stringify(settings, null, 2));
|
||||||
console.log(`\n💾 Backup saved to: ${backupPath}`);
|
console.log(`\n💾 Backup saved to: ${backupPath}`);
|
||||||
|
|
||||||
// Write migrated
|
|
||||||
writeFileSync(settingsPath, JSON.stringify(migrated, null, 2));
|
writeFileSync(settingsPath, JSON.stringify(migrated, null, 2));
|
||||||
console.log(`\n✅ Settings migrated successfully!`);
|
console.log(`\n✅ Settings migrated successfully!`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user