home / skills / mgd34msu / goodvibes-plugin / config-hygiene

config-hygiene skill

/plugins/goodvibes/skills/common/review/config-hygiene

npx playbooks add skill mgd34msu/goodvibes-plugin --skill config-hygiene

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
8.5 KB
---
name: config-hygiene
description: Fixes configuration hygiene issues including gitignore patterns, ESLint config duplication, and hook scripts. Use when encountering backup files in repo, missing gitignore patterns, or config maintenance issues.
---

# Configuration Hygiene Fixes

Fixes for project configuration issues. Clean configs prevent noise, ensure consistency, and improve developer experience.

## Quick Start

1. Identify the config issue (gitignore, ESLint, husky)
2. Apply the recommended fix
3. Verify no unintended side effects
4. Commit config changes

## Priority Matrix

| Issue | Priority | Impact |
|-------|----------|--------|
| Backup files in repo | P2 | Noise, confusion |
| Missing gitignore patterns | P2 | Unwanted files tracked |
| Husky silent failure | P2 | Hidden hook failures |
| ESLint config duplication | P3 | Maintenance burden |
| Permissive lint threshold | P2 | Quality regression |

---

## Workflows

### Backup/Artifact Files in Repository (#35 - 12 occurrences)

**Detection**: Files matching `*.bak`, `*.orig`, `*~` patterns.

**Pattern**: Editor/merge artifacts committed to repo.

**Fix Strategy**: Remove from git, add to gitignore.

```bash
# Remove from git (keep locally)
git rm --cached "*.bak"
git rm --cached "*.orig"
git rm --cached "*~"

# Add to .gitignore
echo "*.bak" >> .gitignore
echo "*.orig" >> .gitignore
echo "*~" >> .gitignore

# Commit
git add .gitignore
git commit -m "chore: ignore backup/artifact files"
```

---

### Missing .gitignore Patterns (#36 - 8 occurrences)

**Detection**: Common patterns not in .gitignore.

**Fix Strategy**: Add comprehensive patterns.

```gitignore
# Editor artifacts
*.swp
*.swo
*~
*.bak
*.orig

# OS files
.DS_Store
Thumbs.db

# IDE
.idea/
.vscode/
*.sublime-*

# Build outputs
dist/
build/
*.tsbuildinfo

# Dependencies
node_modules/

# Environment
.env
.env.*
!.env.example

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Test coverage
coverage/
.nyc_output/

# Cache
.cache/
.eslintcache
.parcel-cache/
```

**Template**: See `templates/gitignore-node.txt` for complete Node.js template.

---

### Husky Prepare Script Silent Failure (#37 - 1 occurrence)

**Detection**: `prepare` script uses `|| true` fallback.

**Pattern**: Hook installation fails silently.

```json
// PROBLEM - hides failures
{
  "scripts": {
    "prepare": "husky install || true"
  }
}
```

**Fix Strategy 1**: Conditional execution based on CI.

```json
// SOLUTION - check CI environment
{
  "scripts": {
    "prepare": "node -e \"if (process.env.CI !== 'true') require('husky').install()\""
  }
}
```

**Fix Strategy 2**: Use is-ci package.

```bash
npm install --save-dev is-ci
```

```json
{
  "scripts": {
    "prepare": "is-ci || husky install"
  }
}
```

**Fix Strategy 3**: Husky v9+ auto-setup.

```json
// Husky v9 uses .husky/_/husky.sh automatically
{
  "scripts": {
    "prepare": "husky"
  }
}
```

---

### Permissive lint:strict Threshold (#38 - 1 occurrence)

**Detection**: High `--max-warnings` value in lint script.

**Pattern**: Too many warnings allowed.

```json
// PROBLEM - too permissive
{
  "scripts": {
    "lint:strict": "eslint . --max-warnings 500"
  }
}
```

**Fix Strategy**: Progressive reduction.

```json
// Week 1: Lower to current count + small buffer
{
  "scripts": {
    "lint:strict": "eslint . --max-warnings 350"
  }
}

// Week 2: Lower further
{
  "scripts": {
    "lint:strict": "eslint . --max-warnings 200"
  }
}

// Target: Zero tolerance
{
  "scripts": {
    "lint:strict": "eslint . --max-warnings 0"
  }
}
```

**Tracking Progress**:

```bash
# Get current warning count
npx eslint . 2>&1 | tail -1
# Output: "X warnings" or "X problems (Y errors, Z warnings)"
```

---

### ESLint Config Duplication (#39, #40)

**Issue #39**: Repeated plugin/globals definitions.

```typescript
// PROBLEM - repeated in multiple blocks
export default [
  {
    files: ['**/*.ts'],
    plugins: { '@typescript-eslint': tsPlugin },
  },
  {
    files: ['**/*.tsx'],
    plugins: { '@typescript-eslint': tsPlugin },  // Duplicated
  },
];
```

**Fix Strategy**: Extract shared config.

```typescript
// SOLUTION - shared base config
const baseTypescriptConfig = {
  plugins: {
    '@typescript-eslint': tsPlugin,
  },
  languageOptions: {
    parser: tsParser,
    parserOptions: {
      project: './tsconfig.json',
    },
  },
};

export default [
  {
    files: ['**/*.ts'],
    ...baseTypescriptConfig,
    rules: { /* ts-specific rules */ },
  },
  {
    files: ['**/*.tsx'],
    ...baseTypescriptConfig,
    rules: { /* tsx-specific rules */ },
  },
];
```

**Issue #40**: Disabled rules duplicated across blocks.

```typescript
// PROBLEM - duplicated disabled rules
{
  rules: {
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-unsafe-assignment': 'off',
  },
},
```

**Fix Strategy**: Extract to named constant with documentation.

```typescript
// SOLUTION - extracted with docs
/**
 * Rules disabled during migration to strict TypeScript.
 * TODO: Re-enable progressively - see issue #456
 */
const migrationDisabledRules = {
  '@typescript-eslint/no-explicit-any': 'off',
  '@typescript-eslint/no-unsafe-assignment': 'off',
  '@typescript-eslint/no-unsafe-member-access': 'off',
} as const;

export default [
  {
    files: ['**/*.ts'],
    rules: {
      ...migrationDisabledRules,
      // ts-specific rules
    },
  },
];
```

---

### Recursive Directory Walking Without Limits (#23 - 1 occurrence)

**Pattern**: Unbounded recursion in file walking.

```typescript
// PROBLEM - no limits
async function walkDir(dir: string): Promise<string[]> {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  const files: string[] = [];
  for (const entry of entries) {
    if (entry.isDirectory()) {
      files.push(...await walkDir(path.join(dir, entry.name))); // No limit!
    }
  }
  return files;
}
```

**Fix Strategy**: Add depth limit and visited tracking.

```typescript
// SOLUTION - with limits
interface WalkOptions {
  maxDepth?: number;
  maxFiles?: number;
}

async function walkDir(
  dir: string,
  options: WalkOptions = {},
  depth = 0,
  visited = new Set<string>()
): Promise<string[]> {
  const { maxDepth = 10, maxFiles = 10000 } = options;

  if (depth > maxDepth) {
    return [];
  }

  const realPath = await fs.realpath(dir);
  if (visited.has(realPath)) {
    return []; // Circular symlink
  }
  visited.add(realPath);

  const entries = await fs.readdir(dir, { withFileTypes: true });
  const files: string[] = [];

  for (const entry of entries) {
    if (files.length >= maxFiles) break;

    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      files.push(...await walkDir(fullPath, options, depth + 1, visited));
    } else {
      files.push(fullPath);
    }
  }

  return files;
}
```

---

### Regex Created Per-Match (#24 - 1 occurrence)

**Pattern**: Regex compiled on every function call.

```typescript
// PROBLEM - regex created every call
function containsKeyword(text: string, keyword: string): boolean {
  const regex = new RegExp(`\\b${keyword}\\b`, 'i');
  return regex.test(text);
}
```

**Fix Strategy**: Pre-compile and cache.

```typescript
// SOLUTION - cached regex
const keywordRegexCache = new Map<string, RegExp>();

function getKeywordRegex(keyword: string): RegExp {
  let regex = keywordRegexCache.get(keyword);
  if (!regex) {
    regex = new RegExp(`\\b${escapeRegex(keyword)}\\b`, 'i');
    keywordRegexCache.set(keyword, regex);
  }
  return regex;
}

function escapeRegex(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function containsKeyword(text: string, keyword: string): boolean {
  return getKeywordRegex(keyword).test(text);
}
```

---

## Scripts

### Check Config Hygiene

```bash
node scripts/check-config.js
```

### Fix Gitignore

```bash
node scripts/fix-gitignore.js
```

---

## Templates

### Node.js .gitignore

See `templates/gitignore-node.txt`

### ESLint Base Config

See `templates/eslint-base.js`

---

## CI/CD Considerations

### Ensuring Hooks Run

```yaml
# GitHub Actions
- name: Install dependencies
  run: npm ci

# This runs "prepare" script which installs husky
# Make sure CI=true is set to skip if needed
```

### Lint in CI

```yaml
- name: Lint
  run: npm run lint:strict
  # Fails if warnings exceed threshold
```

### Progressive Strictness

```yaml
# Allow gradual improvement
- name: Check lint progress
  run: |
    WARNINGS=$(npx eslint . 2>&1 | grep -oP '\d+ warning' | grep -oP '\d+' || echo "0")
    THRESHOLD=200
    if [ "$WARNINGS" -gt "$THRESHOLD" ]; then
      echo "Too many warnings: $WARNINGS (max: $THRESHOLD)"
      exit 1
    fi
```