Skip to content

Commit 2934c5b

Browse files
semsem
authored andcommitted
feat: major v4.0.0 enhancements - checkpoints and hooks
Transform Context Forge into comprehensive AI development platform with human-in-the-loop checkpoints and Claude Code hooks integration. This establishes Context Forge as a comprehensive platform for AI-assisted development with enterprise-grade safeguards.
1 parent 78828b1 commit 2934c5b

File tree

10 files changed

+1227
-0
lines changed

10 files changed

+1227
-0
lines changed

src/cli/commands/copy-hooks.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Command } from 'commander';
2+
import chalk from 'chalk';
3+
import ora from 'ora';
4+
import path from 'path';
5+
import inquirer from 'inquirer';
6+
import { copyHooksFromRepo } from '../../generators/hooks';
7+
import fs from 'fs-extra';
8+
9+
export const copyHooksCommand = new Command('copy-hooks')
10+
.description('Copy Claude Code hooks from an external repository')
11+
.option('-s, --source <path>', 'source repository path')
12+
.option('-t, --target <path>', 'target directory (default: current directory)', '.')
13+
.option('-a, --all', 'copy all hooks without prompting')
14+
.action(async (options) => {
15+
console.log(chalk.blue.bold('\n🪝 Copy Claude Code Hooks\n'));
16+
17+
const spinner = ora();
18+
19+
try {
20+
let sourcePath = options.source;
21+
22+
// If no source provided, prompt for it
23+
if (!sourcePath) {
24+
const { repoPath } = await inquirer.prompt([
25+
{
26+
type: 'input',
27+
name: 'repoPath',
28+
message: 'Path to hooks repository:',
29+
default: '../claude-hooks-repo',
30+
validate: async (input) => {
31+
const exists = await fs.pathExists(input);
32+
return exists || 'Repository path does not exist';
33+
},
34+
},
35+
]);
36+
sourcePath = repoPath;
37+
}
38+
39+
const targetPath = path.resolve(options.target);
40+
const hooksRepoPath = path.resolve(sourcePath);
41+
42+
// Validate source repository
43+
spinner.start('Validating hooks repository...');
44+
const hooksDir = path.join(hooksRepoPath, 'hooks');
45+
46+
if (!(await fs.pathExists(hooksDir))) {
47+
spinner.fail('Hooks directory not found in repository');
48+
console.log(chalk.red(`Expected hooks directory at: ${hooksDir}`));
49+
return;
50+
}
51+
52+
const availableHooks = await fs.readdir(hooksDir);
53+
spinner.succeed(`Found ${availableHooks.length} hooks in repository`);
54+
55+
let selectedHooks: string[] = [];
56+
57+
if (options.all) {
58+
selectedHooks = availableHooks;
59+
} else {
60+
// Let user select which hooks to copy
61+
const { hooks } = await inquirer.prompt([
62+
{
63+
type: 'checkbox',
64+
name: 'hooks',
65+
message: 'Select hooks to copy:',
66+
choices: availableHooks.map((hook) => ({
67+
name: hook,
68+
value: hook,
69+
checked: true, // Default to all selected
70+
})),
71+
},
72+
]);
73+
selectedHooks = hooks;
74+
}
75+
76+
if (selectedHooks.length === 0) {
77+
console.log(chalk.yellow('No hooks selected. Exiting.'));
78+
return;
79+
}
80+
81+
// Copy selected hooks
82+
spinner.start(`Copying ${selectedHooks.length} hooks...`);
83+
await copyHooksFromRepo(hooksRepoPath, targetPath, selectedHooks);
84+
spinner.succeed('Hooks copied successfully');
85+
86+
// Display success message
87+
console.log(chalk.green.bold('\n✨ Hooks installation complete!\n'));
88+
console.log(chalk.white('Copied hooks:'));
89+
selectedHooks.forEach((hook) => {
90+
console.log(chalk.gray(` • ${hook}`));
91+
});
92+
93+
console.log(chalk.blue.bold('\n🎯 Next steps:\n'));
94+
console.log(chalk.white('1. Review hooks in .claude/hooks/'));
95+
console.log(chalk.white('2. Customize hooks for your project'));
96+
console.log(chalk.white('3. Ensure Claude Code has hooks enabled'));
97+
console.log(chalk.white('4. Test hooks by running development commands\n'));
98+
99+
// Show hook status
100+
const targetHooksPath = path.join(targetPath, '.claude', 'hooks');
101+
const installedHooks = await fs.readdir(targetHooksPath);
102+
console.log(chalk.cyan(`📁 Hooks directory: ${targetHooksPath}`));
103+
console.log(chalk.cyan(`📊 Total hooks installed: ${installedHooks.length}`));
104+
} catch (error) {
105+
spinner.fail('Failed to copy hooks');
106+
throw error;
107+
}
108+
});

src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { initCommand } from './commands/init';
44
import { analyzeCommand } from './commands/analyze';
55
import { validateCommand } from '../commands/validate';
66
import { runPrpCommand } from './commands/run-prp';
7+
import { copyHooksCommand } from './commands/copy-hooks';
78
import { version } from '../../package.json';
89

910
const program = new Command();
@@ -21,6 +22,7 @@ program.addCommand(initCommand);
2122
program.addCommand(analyzeCommand);
2223
program.addCommand(validateCommand);
2324
program.addCommand(runPrpCommand);
25+
program.addCommand(copyHooksCommand);
2426

2527
// Error handling wrapper
2628
const handleError = (error: Error) => {
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import inquirer from 'inquirer';
2+
import { CheckpointConfig, CustomMilestone } from '../../types';
3+
import { getDefaultCheckpointTriggers } from '../../generators/checkpointCommands';
4+
5+
export async function checkpointConfig(projectType: string): Promise<CheckpointConfig | undefined> {
6+
console.log('\n🛑 Checkpoint System Configuration:\n');
7+
8+
const { enableCheckpoints } = await inquirer.prompt([
9+
{
10+
type: 'confirm',
11+
name: 'enableCheckpoints',
12+
message: 'Enable Human-in-the-Loop checkpoint system?',
13+
default: false,
14+
},
15+
]);
16+
17+
if (!enableCheckpoints) {
18+
return undefined;
19+
}
20+
21+
console.log('\n The checkpoint system will pause development at critical milestones');
22+
console.log(' and request human verification before proceeding.\n');
23+
24+
const { triggerTypes } = await inquirer.prompt([
25+
{
26+
type: 'checkbox',
27+
name: 'triggerTypes',
28+
message: 'Which types of milestones should trigger checkpoints?',
29+
choices: [
30+
{
31+
name: 'Database connections and schema changes',
32+
value: 'database-connection',
33+
checked: true,
34+
},
35+
{
36+
name: 'Authentication and security implementations',
37+
value: 'authentication-setup',
38+
checked: true,
39+
},
40+
{
41+
name: 'API endpoints that modify data',
42+
value: 'api-endpoints',
43+
checked: true,
44+
},
45+
{
46+
name: 'External service integrations',
47+
value: 'integration-setup',
48+
checked: false,
49+
},
50+
{
51+
name: 'Production deployment configurations',
52+
value: 'production-deployment',
53+
checked: true,
54+
},
55+
...(projectType === 'fullstack' || projectType === 'api'
56+
? [
57+
{
58+
name: 'Data migrations and transformations',
59+
value: 'data-migration',
60+
checked: true,
61+
},
62+
]
63+
: []),
64+
],
65+
},
66+
]);
67+
68+
const { customMilestones } = await inquirer.prompt([
69+
{
70+
type: 'confirm',
71+
name: 'customMilestones',
72+
message: 'Add custom project-specific milestones?',
73+
default: false,
74+
},
75+
]);
76+
77+
// Get default triggers and filter by selected types
78+
const allTriggers = getDefaultCheckpointTriggers(projectType);
79+
const selectedTriggers = allTriggers.filter((trigger) => triggerTypes.includes(trigger.id));
80+
81+
const config: CheckpointConfig = {
82+
enabled: true,
83+
triggers: selectedTriggers,
84+
};
85+
86+
// Add custom milestones if requested
87+
if (customMilestones) {
88+
config.customMilestones = await collectCustomMilestones();
89+
}
90+
91+
return config;
92+
}
93+
94+
async function collectCustomMilestones(): Promise<CustomMilestone[]> {
95+
const milestones: CustomMilestone[] = [];
96+
97+
console.log('\n Define custom milestones for your project:');
98+
99+
let addMore = true;
100+
while (addMore) {
101+
const milestone = await inquirer.prompt([
102+
{
103+
type: 'input',
104+
name: 'name',
105+
message: 'Milestone name:',
106+
validate: (input) => input.length > 0 || 'Please enter a milestone name',
107+
},
108+
{
109+
type: 'input',
110+
name: 'description',
111+
message: 'Description:',
112+
validate: (input) => input.length > 0 || 'Please enter a description',
113+
},
114+
{
115+
type: 'input',
116+
name: 'testInstructions',
117+
message: 'Test instructions (comma-separated):',
118+
filter: (input) =>
119+
input
120+
.split(',')
121+
.map((s: string) => s.trim())
122+
.filter((s: string) => s.length > 0),
123+
},
124+
{
125+
type: 'confirm',
126+
name: 'blocksUntilApproved',
127+
message: 'Should this milestone BLOCK development until approved?',
128+
default: true,
129+
},
130+
]);
131+
132+
const verificationPoints = [
133+
`${milestone.name} functionality works as expected`,
134+
'No breaking changes introduced',
135+
'Performance is acceptable',
136+
];
137+
138+
const fullMilestone = {
139+
...milestone,
140+
verificationPoints,
141+
};
142+
143+
milestones.push(fullMilestone);
144+
145+
const { continueAdding } = await inquirer.prompt([
146+
{
147+
type: 'confirm',
148+
name: 'continueAdding',
149+
message: 'Add another custom milestone?',
150+
default: false,
151+
},
152+
]);
153+
154+
addMore = continueAdding;
155+
}
156+
157+
return milestones;
158+
}
159+
160+
export function generateCheckpointSummary(config: CheckpointConfig): string {
161+
if (!config.enabled) {
162+
return 'Checkpoint system: Disabled';
163+
}
164+
165+
const triggerCount = config.triggers.length;
166+
const customCount = config.customMilestones?.length || 0;
167+
const criticalTriggers = config.triggers.filter((t) => t.category === 'critical').length;
168+
169+
return `Checkpoint system: Enabled
170+
- ${triggerCount} automatic triggers (${criticalTriggers} critical)
171+
- ${customCount} custom milestones
172+
- Human verification required at key development points`;
173+
}

src/cli/prompts/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { prdInput } from './prdInput';
55
import { techStack } from './techStack';
66
import { features } from './features';
77
import { projectConfig } from './projectConfig';
8+
import { checkpointConfig } from './checkpointConfig';
89

910
export async function runPrompts(): Promise<ProjectConfig> {
1011
// Step 1: Basic project information
@@ -25,12 +26,19 @@ export async function runPrompts(): Promise<ProjectConfig> {
2526
// Step 6: Project configuration
2627
const config = await projectConfig();
2728

29+
// Step 7: Checkpoint configuration (if enabled)
30+
let checkpoints = undefined;
31+
if (config.extras.checkpoints) {
32+
checkpoints = await checkpointConfig(basicInfo.projectType);
33+
}
34+
2835
return {
2936
...basicInfo,
3037
targetIDEs,
3138
prd,
3239
techStack: stack,
3340
features: selectedFeatures,
3441
...config,
42+
checkpointConfig: checkpoints,
3543
};
3644
}

src/cli/prompts/projectConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface ProjectConfigAnswers {
1313
prp?: boolean;
1414
aiDocs?: boolean;
1515
claudeCommands?: boolean;
16+
hooks?: boolean;
17+
checkpoints?: boolean;
1618
};
1719
}
1820

@@ -83,6 +85,8 @@ export async function projectConfig(): Promise<ProjectConfigAnswers> {
8385
{ name: 'PRP (Product Requirement Prompts)', value: 'prp' },
8486
{ name: 'AI Documentation directory', value: 'aiDocs' },
8587
{ name: 'Claude Code commands', value: 'claudeCommands' },
88+
{ name: 'Claude Code hooks integration', value: 'hooks' },
89+
{ name: 'Human-in-the-Loop Checkpoints', value: 'checkpoints' },
8690
],
8791
},
8892
]);
@@ -97,6 +101,8 @@ export async function projectConfig(): Promise<ProjectConfigAnswers> {
97101
prp: selectedExtras.includes('prp'),
98102
aiDocs: selectedExtras.includes('aiDocs'),
99103
claudeCommands: selectedExtras.includes('claudeCommands'),
104+
hooks: selectedExtras.includes('hooks'),
105+
checkpoints: selectedExtras.includes('checkpoints'),
100106
};
101107

102108
return {

0 commit comments

Comments
 (0)