Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export default class Init extends Command {
static description = `Initialize Claude Code hooks in your project

This command sets up basic Claude Code hooks in your project:
• Creates settings.json with default hook configuration
• Generates index.ts with session-saving handlers for all hook types
• Creates settings.json (or settings.json.local with --local flag) with hook configuration
• Generates index.ts with session-saving handlers for all hook types (including SubagentStop)
• Creates lib.ts with base utilities for hook management
• Saves session data to system temp directory`

Expand All @@ -23,6 +23,10 @@ This command sets up basic Claude Code hooks in your project:
description: 'Overwrite existing hooks',
command: '<%= config.bin %> <%= command.id %> --force',
},
{
description: 'Create local settings file',
command: '<%= config.bin %> <%= command.id %> --local',
},
]

static flags = {
Expand All @@ -31,6 +35,11 @@ This command sets up basic Claude Code hooks in your project:
description: 'Overwrite existing hooks without prompting',
helpGroup: 'GLOBAL',
}),
local: Flags.boolean({
char: 'l',
description: 'Create settings.json.local instead of settings.json',
helpGroup: 'GLOBAL',
}),
}

public async run(): Promise<void> {
Expand Down Expand Up @@ -70,7 +79,7 @@ This command sets up basic Claude Code hooks in your project:
await this.generateHookFiles()

// Update or create settings.json
await this.updateSettings()
await this.updateSettings(flags.local)

// Install required dependencies
spinner.text = 'Installing dependencies...'
Expand All @@ -80,6 +89,9 @@ This command sets up basic Claude Code hooks in your project:

// Success message
console.log(chalk.green('\n✨ Claude Code hooks initialized!\n'))
if (flags.local) {
console.log(chalk.yellow('📝 Created settings.json.local for personal configuration\n'))
}
console.log(chalk.gray('Next steps:'))
console.log(chalk.gray('1. Ensure Bun is installed (Bun is required to run Claude hooks)'))
console.log(chalk.gray('2. Edit .claude/hooks/index.ts to customize hook behavior'))
Expand Down Expand Up @@ -200,8 +212,8 @@ This command sets up basic Claude Code hooks in your project:
})
}

private async updateSettings(): Promise<void> {
const settingsPath = '.claude/settings.json'
private async updateSettings(useLocal = false): Promise<void> {
const settingsPath = useLocal ? '.claude/settings.json.local' : '.claude/settings.json'
let settings: any = {}

try {
Expand Down Expand Up @@ -261,6 +273,17 @@ This command sets up basic Claude Code hooks in your project:
],
},
],
SubagentStop: [
{
matcher: '',
hooks: [
{
type: 'command',
command: 'bun .claude/hooks/index.ts SubagentStop',
},
],
},
],
}

await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2))
Expand Down
52 changes: 52 additions & 0 deletions test/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ describe('init', () => {
const {stdout} = await runCommand(['init', '--help'])
expect(stdout).to.contain('Initialize Claude Code hooks')
expect(stdout).to.contain('--force')
expect(stdout).to.contain('--local')
} catch (_error) {
// Fallback to testing with execSync for compiled version
const output = execSync(`node ${binPath} init --help`, {encoding: 'utf8'})
expect(output).to.contain('Initialize Claude Code hooks')
expect(output).to.contain('--force')
expect(output).to.contain('--local')
}
})
})
Expand Down Expand Up @@ -75,12 +77,19 @@ describe('init', () => {
expect(settings.hooks).to.have.property('Stop')
expect(settings.hooks).to.have.property('PreToolUse')
expect(settings.hooks).to.have.property('PostToolUse')
expect(settings.hooks).to.have.property('SubagentStop')

// Check command structure
expect(settings.hooks.PreToolUse[0].hooks[0]).to.deep.equal({
type: 'command',
command: 'bun .claude/hooks/index.ts PreToolUse',
})

// Check SubagentStop command structure
expect(settings.hooks.SubagentStop[0].hooks[0]).to.deep.equal({
type: 'command',
command: 'bun .claude/hooks/index.ts SubagentStop',
})
})

it('generates correct index.ts content', async () => {
Expand Down Expand Up @@ -154,6 +163,49 @@ describe('init', () => {
})
})

describe('--local flag', () => {
it('creates settings.json.local instead of settings.json', async () => {
execSync(`node ${binPath} init --local`, {
cwd: testDir,
encoding: 'utf8',
})

// Check that settings.json.local was created
expect(await fs.pathExists(path.join(testDir, '.claude/settings.json.local'))).to.be.true
expect(await fs.pathExists(path.join(testDir, '.claude/settings.json'))).to.be.false

// Verify the content
const settings = await fs.readJson(path.join(testDir, '.claude/settings.json.local'))
expect(settings.hooks).to.have.property('SubagentStop')
})

it('shows local flag message in output', async () => {
const output = execSync(`node ${binPath} init --local`, {
cwd: testDir,
encoding: 'utf8',
})

expect(output).to.contain('Created settings.json.local for personal configuration')
})

it('works with --force flag', async () => {
// Create initial hooks
execSync(`node ${binPath} init --local`, {
cwd: testDir,
encoding: 'utf8',
})

// Force overwrite with local flag
const output = execSync(`node ${binPath} init --local --force`, {
cwd: testDir,
encoding: 'utf8',
})

expect(output).to.contain('Claude Code hooks initialized')
expect(output).to.contain('Created settings.json.local for personal configuration')
})
})

describe('error handling', () => {
it('handles permission errors gracefully', async function () {
// Skip on Windows
Expand Down