Skip to content
Open
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
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ ghouls all [--dry-run] [owner/repo] # Clean both
```

Uses GitHub CLI auth (`gh auth login`). TypeScript/Node.js/pnpm project.

### AI Team Assignments

| Task | Agent | Notes |
| --------------------------------------- | ------------------------ | -------------------------------------------- |
| Code reviews and quality assurance | code-reviewer | Required for all PRs and feature changes |
| Performance optimization and profiling | performance-optimizer | Essential for CLI tool responsiveness |
| Backend development and API integration | backend-developer | Handles GitHub API integration and CLI logic |
| API design and GitHub integration specs | api-architect | Designs interfaces for GitHub API wrapper |
| Documentation updates and maintenance | documentation-specialist | Maintains README, API docs, and user guides |
113 changes: 113 additions & 0 deletions NEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Next Steps for Zod 4 Config Validation PR

## Priority Improvements for this PR

### 1. **Fix Remaining Test Failures (4 remaining)** 🔥

The memory issue is solved, but 4 tests are still failing due to mock setup issues:

```bash
# Run this to see the exact failures:
pnpm test src/utils/configLoader.test.ts
```

**Specific fixes needed:**

- Fix `find-up` mock in tests to properly simulate git directory discovery
- Update test expectations for the new config loading behavior
- Fix one validation error message test (Zod vs manual validation message differences)

### 2. **Clean Up Test Mocks**

The test file still has complex mocking that could be simplified:

- Remove the old path resolution mocks that were causing the infinite loop
- Simplify the `find-up` mocking strategy
- Consider using more realistic mock data

### 3. **Add Integration Tests**

Create a simple integration test that:

- Tests actual config file loading without mocks
- Verifies Zod validation works end-to-end
- Tests the find-up functionality in a real directory structure

### 4. **Documentation Updates**

Update the project documentation to reflect:

- New Zod validation capabilities
- Better error messages for config validation
- The `find-up` dependency and why it was added

### 5. **Consider Performance Optimization**

While not critical, consider:

- Caching config file discovery results
- Lazy loading Zod schemas if they're large
- Add benchmarks to ensure config loading remains fast

## What's Already Working Well ✅

- **Memory issue completely resolved** - No more infinite loops
- **Zod 4 integration working** - Type-safe validation with great error messages
- **Core functionality intact** - All config loading, merging, and validation works
- **24/28 tests passing** - The majority of functionality is properly tested

## Command to Verify Success

```bash
# This should complete without memory issues and show only 4 minor test failures:
pnpm test src/utils/configLoader.test.ts

# This should show all core functionality working:
pnpm test src/types/configSchema.test.ts src/types/config.test.ts

# This should compile without errors:
pnpm compile
```

## Changes Made in This PR

### ✅ Completed

1. **Installed Zod 4.0.17** as a dependency
2. **Created comprehensive Zod schemas** (`src/types/configSchema.ts`)
3. **Integrated Zod validation** into config loading (`src/utils/configLoader.ts`)
4. **Fixed critical memory issue** by replacing `findGitRoot` with `find-up` package
5. **Enhanced error handling** with detailed validation messages
6. **Added extensive tests** for Zod validation (18 new tests)

### 🔧 Technical Details

- **Memory Issue Root Cause**: Infinite loop in `findGitRoot` due to faulty path resolution mocks
- **Solution**: Replaced custom directory traversal with battle-tested `find-up` package
- **Validation Improvement**: ~100 lines of manual validation replaced with concise Zod schemas
- **Type Safety**: Automatic TypeScript type inference from Zod schemas

## File Changes Summary

### New Files

- `src/types/configSchema.ts` - Zod schemas for config validation
- `src/types/configSchema.test.ts` - Comprehensive tests for Zod validation
- `NEXT.md` - This file

### Modified Files

- `src/utils/configLoader.ts` - Integrated Zod validation and find-up
- `src/utils/configLoader.test.ts` - Updated tests for new validation system
- `package.json` - Added `zod@^4.0.17` and `find-up@^7.0.0` dependencies

## Testing Status

```
✅ src/types/configSchema.test.ts (18 tests) - All passing
✅ src/types/config.test.ts (13 tests) - All passing
✅ src/utils/branchSafetyChecks.test.ts (55 tests) - All passing
⚠️ src/utils/configLoader.test.ts (24/28 tests) - 4 minor failures remain
```

The PR is in great shape - just needs those final test fixes to be merge-ready! 🚀
113 changes: 113 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,116 @@ pnpm test:coverage
```

The test suite includes comprehensive unit tests covering all core functionality, utilities, and edge cases.

# Configuration

Ghouls supports optional configuration to customize which branches are protected from deletion.

## Configuration File Locations

Ghouls looks for configuration files in the following order (first found takes precedence):

1. **Environment variable**: `GHOULS_CONFIG=/path/to/config.json`
2. **Repository root**: `.config/ghouls.json`
3. **User home**: `~/.config/ghouls/config.json`

## Configuration Format

```json
{
"protectedBranches": ["main", "master", "production"]
}
```

## Configuration Options

### `protectedBranches` (optional array of strings)

List of branch names and patterns that should never be deleted (case-insensitive). Supports both exact branch names and glob patterns (e.g., `"release/*"`, `"hotfix-*"`).

**Default protected branches**: `["main", "master", "develop", "dev", "staging", "production", "prod", "release/*", "release-*", "hotfix/*"]`

### Extending vs Replacing Default Protection

You can either **replace** the default protected branches entirely, or **extend** them with additional custom branches using the `$GHOULS_DEFAULT` placeholder.

#### Replace (Default Behavior)

When you specify `protectedBranches` without the `$GHOULS_DEFAULT` placeholder, your configuration completely replaces the default protected branches:

```json
{
"protectedBranches": ["main", "production"]
}
```

This will **only** protect `main` and `production` branches, ignoring all other default protections.

#### Extend with `$GHOULS_DEFAULT`

To keep all the default protected branches and add your own custom ones, use the `$GHOULS_DEFAULT` placeholder:

```json
{
"protectedBranches": ["$GHOULS_DEFAULT", "custom-branch", "feature-*"]
}
```

The `$GHOULS_DEFAULT` placeholder gets expanded to include all default protected branches. You can position it anywhere in the array:

```json
{
"protectedBranches": ["urgent-*", "$GHOULS_DEFAULT", "experimental"]
}
```

This approach is similar to Turborepo's `$TURBO_DEFAULT$` syntax and allows you to extend rather than replace the defaults.

### Benefits of Using `$GHOULS_DEFAULT`

- **Future-proof**: Automatically includes new default protections added in future Ghouls versions
- **Safer**: Reduces risk of accidentally removing important default protections
- **Cleaner**: Avoids duplicating the full list of default branches in your config
- **Flexible**: Can be positioned anywhere in your array for custom ordering

## Examples

### Replace with custom branches only

```json
{
"protectedBranches": ["main", "production", "staging"]
}
```

### Extend defaults with custom branches

```json
{
"protectedBranches": ["$GHOULS_DEFAULT", "custom-branch", "feature-*"]
}
```

### Custom branches with defaults at the end

```json
{
"protectedBranches": ["urgent-*", "experimental", "$GHOULS_DEFAULT"]
}
```

### Minimal protection (main branch only)

```json
{
"protectedBranches": ["main"]
}
```

### Use only defaults (explicit)

```json
{
"protectedBranches": ["$GHOULS_DEFAULT"]
}
```
6 changes: 6 additions & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://unpkg.com/knip@5/schema-jsonc.json",
"entry": [
"src/cli.ts",
],
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
"execa": "^9.6.0",
"find-up": "^7.0.0",
"inquirer": "^12.9.0",
"micromatch": "^4.0.8",
"progress": "^2.0.3",
"source-map-support": "^0.5.21",
"yargs": "^18.0.0",
"zod": "^4.0.17"
},
"devDependencies": {
"@types/inquirer": "^9.0.8",
"@types/micromatch": "^4.0.9",
"@types/node": "^22.17.0",
"@types/progress": "^2.0.7",
"@types/source-map-support": "^0.5.10",
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 13 additions & 2 deletions src/commands/PruneLocalBranches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ProgressBar from "progress";
import type { CommandModule } from "yargs";
import { OctokitPlus, PullRequest } from "../OctokitPlus.js";
import { filterSafeBranches } from "../utils/branchSafetyChecks.js";
import { loadConfigSafe } from "../utils/configLoader.js";
import { createOctokitPlus } from "../utils/createOctokitPlus.js";
import { getGitRemote } from "../utils/getGitRemote.js";
import { deleteLocalBranch, getCurrentBranch, getLocalBranches, isGitRepository } from "../utils/localGitOperations.js";
Expand Down Expand Up @@ -102,6 +103,9 @@ class PruneLocalBranches {
public async perform() {
console.log(`\nScanning for local branches that can be safely deleted...`);

// Load configuration
const config = loadConfigSafe(true); // Log errors if config loading fails

// Get all local branches
const localBranches = getLocalBranches();
const currentBranch = getCurrentBranch();
Expand All @@ -119,7 +123,12 @@ class PruneLocalBranches {
console.log(`Found ${mergedPRs.size} merged pull requests`);

// Filter branches for safety
const branchAnalysis = filterSafeBranches(localBranches, currentBranch, mergedPRs);
const branchAnalysis = filterSafeBranches(
localBranches,
currentBranch,
mergedPRs,
config,
);
const safeBranches = branchAnalysis.filter(analysis => analysis.safetyCheck.safe);
const unsafeBranches = branchAnalysis.filter(analysis => !analysis.safetyCheck.safe);

Expand Down Expand Up @@ -199,7 +208,9 @@ class PruneLocalBranches {
const prInfo = matchingPR ? `#${matchingPR.number}` : "no PR";

if (bar) {
bar.update(deletedCount + errorCount, { branch: `${branch.name} (${prInfo})` });
bar.update(deletedCount + errorCount, {
branch: `${branch.name} (${prInfo})`,
});
}

try {
Expand Down
Loading