Skip to content

Commit 21a152c

Browse files
author
catlog22
committed
feat(install): enhance hook installation options with recommended and custom selections
1 parent 1efef96 commit 21a152c

2 files changed

Lines changed: 58 additions & 28 deletions

File tree

.claude/commands/workflow-tune.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Input → Parse → GenTestTask → Confirm → Setup
3232
4. **Sandbox Isolation**. 全部执行在 `sandbox/` 目录(独立 git 仓库),不影响真实项目。
3333
5. **State Machine**. 通过 `current_step` + `current_phase` 推进,禁止同步循环。
3434
6. **ABSOLUTE PATHS for --cd**. `ccw cli --cd` 必须使用绝对路径。相对路径会被 ccw cli 再次拼接 CWD 导致路径重复。`workDir`/`sandboxDir` 在创建时就解析为绝对路径。
35+
7. **FIXED --rule VALUES**. `--rule` 值已硬编码在各 Phase 代码中,禁止替换为其他模板。Execute=`workflow-tune-execute`,Analyze=`analysis-review-code-quality`,Synthesize=`analysis-review-architecture`
3536

3637
## Input Formats
3738

ccw/src/commands/install.ts

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -340,31 +340,65 @@ export async function installCommand(options: InstallOptions): Promise<void> {
340340

341341
divider();
342342

343-
// Hooks installation selection (two-level: none / full)
344-
const { installHooks } = await inquirer.prompt([{
343+
// Hooks installation selection
344+
const RECOMMENDED_HOOKS = ['ccw-coordinator-tracker', 'ccw-coordinator-skill-context'];
345+
346+
const { hookMode } = await inquirer.prompt([{
345347
type: 'list',
346-
name: 'installHooks',
348+
name: 'hookMode',
347349
message: 'Install Claude Code hooks?',
348350
choices: [
349-
{
350-
name: [
351-
`${chalk.cyan('Full')} — install CCW hooks to settings.json (recommended)`,
352-
chalk.gray(' ccw-coordinator-tracker Stop — check /ccw chain progress on response end, write bridge'),
353-
chalk.gray(' ccw-coordinator-skill-context UserPromptSubmit — show active coordinator progress on /ccw input'),
354-
].join('\n'),
355-
value: 'full'
356-
},
357-
{
358-
name: `${chalk.gray('None')} — skip hooks installation`,
359-
value: 'none'
360-
}
351+
{ name: `${chalk.cyan('Recommended')} — CCW coordinator hooks (2 hooks)`, value: 'recommended' },
352+
{ name: `${chalk.cyan('All')} — install all available hooks (22 hooks)`, value: 'all' },
353+
{ name: `${chalk.yellow('Custom')} — select individual hooks`, value: 'custom' },
354+
{ name: `${chalk.gray('None')} — skip hooks installation`, value: 'none' },
361355
]
362356
}]);
363357

358+
let selectedHookIds: string[] = [];
359+
if (hookMode === 'recommended') {
360+
selectedHookIds = [...RECOMMENDED_HOOKS];
361+
} else if (hookMode === 'all') {
362+
const { getAllTemplates } = await import('../core/hooks/hook-templates.js');
363+
selectedHookIds = getAllTemplates().map(t => t.id);
364+
} else if (hookMode === 'custom') {
365+
const { listTemplatesByCategory } = await import('../core/hooks/hook-templates.js');
366+
const categoryNames: Record<string, string> = {
367+
notification: 'Notification',
368+
automation: 'Automation',
369+
protection: 'Protection',
370+
indexing: 'Indexing',
371+
utility: 'Utility',
372+
};
373+
const byCategory = listTemplatesByCategory();
374+
const checklistChoices: Array<{ name: string; value: string; checked?: boolean }> = [];
375+
376+
for (const [category, templates] of Object.entries(byCategory)) {
377+
if (templates.length === 0) continue;
378+
checklistChoices.push({ name: `── ${categoryNames[category] || category} ──`, value: `__sep_${category}`, checked: false });
379+
for (const t of templates) {
380+
checklistChoices.push({
381+
name: ` ${chalk.yellow(t.id)}${t.description} (${t.trigger}${t.matcher ? ` / ${t.matcher}` : ''})`,
382+
value: t.id,
383+
checked: RECOMMENDED_HOOKS.includes(t.id),
384+
});
385+
}
386+
}
387+
388+
const { selectedHooks } = await inquirer.prompt([{
389+
type: 'checkbox',
390+
name: 'selectedHooks',
391+
message: 'Select hooks to install:',
392+
choices: checklistChoices,
393+
pageSize: 20,
394+
}]);
395+
selectedHookIds = selectedHooks.filter((id: string) => !id.startsWith('__sep_'));
396+
}
397+
364398
let hooksInstalled: string[] = [];
365-
if (installHooks === 'full') {
366-
const hookSpinner = createSpinner('Installing Claude Code hooks...').start();
367-
hooksInstalled = installCcwHooks(mode === 'Global' ? homedir() : installPath);
399+
if (selectedHookIds.length > 0) {
400+
const hookSpinner = createSpinner(`Installing ${selectedHookIds.length} Claude Code hooks...`).start();
401+
hooksInstalled = installSelectedHooks(selectedHookIds, mode === 'Global' ? homedir() : installPath);
368402
hookSpinner.succeed(`Installed ${hooksInstalled.length} hooks: ${hooksInstalled.join(', ')}`);
369403
}
370404

@@ -1508,28 +1542,23 @@ export async function installSkillHubCommand(options: SkillHubInstallOptions): P
15081542
}
15091543

15101544
/**
1511-
* Install CCW hooks to settings.json
1545+
* Install selected hooks to settings.json
15121546
*
1513-
* Installs a curated set of hooks for CCW coordinator tracking and monitoring.
1514-
* Reuses the template system from hook-templates.ts.
1547+
* Installs the specified hook templates to settings.json using the template system.
15151548
*
1549+
* @param templateIds - List of hook template IDs to install
15161550
* @param settingsDir - Directory containing settings.json (e.g. ~/.claude/ or project/.claude/)
15171551
* @returns List of installed hook IDs
15181552
*/
1519-
function installCcwHooks(settingsDir: string): string[] {
1553+
function installSelectedHooks(templateIds: string[], settingsDir: string): string[] {
15201554
// Dynamic import to avoid top-level dependency on hook-templates
15211555
// eslint-disable-next-line @typescript-eslint/no-require-imports
15221556
const { installTemplateToSettings } = require('../core/hooks/hook-templates.js') as typeof import('../core/hooks/hook-templates.js');
15231557

1524-
const templates = [
1525-
'ccw-coordinator-tracker', // PostToolUse — track /ccw and /ccw-coordinator progress
1526-
'ccw-coordinator-skill-context', // UserPromptSubmit — inject progress hints when invoking /ccw
1527-
];
1528-
15291558
const installed: string[] = [];
15301559
const scope = settingsDir === homedir() ? 'global' : 'project';
15311560

1532-
for (const templateId of templates) {
1561+
for (const templateId of templateIds) {
15331562
try {
15341563
const result = installTemplateToSettings(templateId, scope);
15351564
if (result.success && !result.message.includes('already')) {

0 commit comments

Comments
 (0)