Skill: felipeoff/sonarqube-analyzer (version 0.1.2)

# SonarQube Analyzer Skill Analisa projetos no SonarQube self-hosted, obtém issues e sugere soluções automatizadas. ## Ferramentas Registradas ### `sonar_get_issues` Obtém lista de issues de um projeto/PR no SonarQube. **Parâmetros:** - `projectKey` (string, obrigatório): Chave do projeto - `pullRequest` (string, opcional): Número da PR para análise específica - `severities` (string[], opcional): Severidades a filtrar (BLOCKER, CRITICAL, MAJOR, MINOR, INFO) - `status` (string, opcional): Status das issues (OPEN, CONFIRMED, FALSE_POSITIVE, etc.) - `limit` (number, opcional): Limite de issues (padrão: 100) **Exemplo:** ```json { "projectKey": "openclaw-panel", "pullRequest": "5", "severities": ["CRITICAL", "MAJOR"], "limit": 50 } ``` ### `sonar_analyze_and_suggest` Analisa issues e sugere soluções com base nas regras do SonarQube. **Parâmetros:** - `projectKey` (string, obrigatório): Chave do projeto - `pullRequest` (string, opcional): Número da PR - `autoFix` (boolean, opcional): Tentar aplicar correções automáticas (padrão: false) **Exemplo:** ```json { "projectKey": "openclaw-panel", "pullRequest": "5", "autoFix": false } ``` ### `sonar_quality_gate` Verifica o status do Quality Gate de um projeto. **Parâmetros:** - `projectKey` (string, obrigatório): Chave do projeto - `pullRequest` (string, opcional): Número da PR **Exemplo:** ```json { "projectKey": "openclaw-panel", "pullRequest": "5" } ``` ## Configuração O skill usa as seguintes configurações do ambiente: ```bash SONAR_HOST_URL=http://127.0.0.1:9000 # URL do SonarQube SONAR_TOKEN=admin # Token de autenticação ``` ## Uso ### Analisar uma PR específica: ```bash node scripts/analyze.js --project=my-project --pr=5 ``` ### Gerar relatório de issues: ```bash node scripts/report.js --project=my-project --format=markdown ``` ### Verificar Quality Gate: ```bash node scripts/quality-gate.js --project=my-project --pr=5 ``` ## Estrutura de Resposta ### sonar_get_issues ```json { "total": 12, "issues": [ { "key": "...", "severity": "MAJOR", "component": "apps/web/src/ui/App.tsx", "line": 346, "message": "Extract this nested ternary...", "rule": "typescript:S3358", "status": "OPEN", "solution": "Extract nested ternary into a separate function..." } ], "summary": { "BLOCKER": 0, "CRITICAL": 0, "MAJOR": 2, "MINOR": 10, "INFO": 0 } } ``` ### sonar_analyze_and_suggest ```json { "projectKey": "openclaw-panel", "analysis": { "totalIssues": 12, "fixableAutomatically": 8, "requiresManualFix": 4 }, "suggestions": [ { "file": "apps/web/src/ui/App.tsx", "line": 346, "issue": "Nested ternary operation", "suggestion": "Extract into independent component", "codeExample": "...", "autoFixable": false } ], "nextSteps": [ "Run lint:fix for auto-fixable issues", "Refactor nested ternaries in App.tsx", "Replace || with ?? operators" ] } ``` ## Soluções Automáticas Disponíveis | Regra | Problema | Solução Automática | |-------|----------|-------------------| | S6606 | Use `\|\|` instead of `??` | ✅ Substituir por `??` | | S3358 | Nested ternary | ❌ Requer refatoração manual | | S6749 | Redundant fragment | ✅ Remover fragment | | S6759 | Non-readonly props | ✅ Adicionar `readonly` | | S3776 | Cognitive complexity | ❌ Requer extração de componentes | | S6571 | `any` in union type | ✅ Remover redundância | ## Requisitos - Node.js 18+ - Acesso ao SonarQube (localhost:9000) - Token de autenticação configurado ## Integração com Workflows Exemplo de uso em GitHub Actions: ```yaml - name: Analyze with SonarQube Skill run: | npm install -g @felipeoff/sonarqube-analyzer sonarqube-analyzer \ --project=my-project \ --pr=${{ github.event.pull_request.number }} \ --suggest-fixes ```

path: CHANGELOG.md
content: # Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-02-07

### Added
- Initial release of SonarQube Analyzer skill
- Support for fetching issues from SonarQube API
- Intelligent rule analysis with suggested solutions
- Auto-fixable vs manual-fix classification
- JSON and Markdown report generation
- CLI tool for command-line usage
- Support for pull request analysis
- Quality Gate status checking
- Support for TypeScript rules:
  - S3358 (Nested ternary)
  - S6606 (Nullish coalescing)
  - S6749 (Redundant fragment)
  - S6759 (Readonly props)
  - S3776 (Cognitive complexity)
  - S6571 (Redundant any)
- Comprehensive documentation
- OpenClaw integration with plugin manifest

[1.0.0]: https://github.com/FelipeOFF/sonarqube-analyzer/releases/tag/v1.0.0

path: CONTRIBUTING.md
content: # Contributing to SonarQube Analyzer

Thank you for your interest in contributing to the SonarQube Analyzer skill! This document provides guidelines and instructions for contributing.

## Development Setup

```bash
# Clone the repository
git clone https://github.com/FelipeOFF/sonarqube-analyzer.git
cd sonarqube-analyzer

# Install dependencies
npm install

# Run tests
npm test

# Run linter
npm run lint
```

## Adding New Rules

To add support for a new SonarQube rule:

1. Edit `src/rules.js`
2. Add entry to `RULE_SOLUTIONS` object:

```javascript
'rule-key': {
  title: 'Human-readable title',
  description: 'Detailed explanation',
  autoFixable: true | false,
  solution: (issue) => `Solution for ${issue.component}:${issue.line}`,
  example: `
// ❌ Before:
bad code

// ✅ After:
good code
  `
}
```

## Pull Request Process

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/my-feature`
3. Make your changes
4. Add tests if applicable
5. Run `npm test` and `npm run lint`
6. Commit with clear messages
7. Push to your fork
8. Open a PR against `main`

## Code Style

- Use ESLint configuration
- Follow existing code patterns
- Add JSDoc comments for functions
- Keep functions small and focused

## Reporting Issues

When reporting issues, please include:
- Node.js version
- SonarQube version
- Error messages
- Steps to reproduce

## License

By contributing, you agree that your contributions will be licensed under the MIT License.

path: README.md
content: # OpenClaw SonarQube Analyzer

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen)](https://nodejs.org/)
[![OpenClaw Plugin](https://img.shields.io/badge/OpenClaw-Plugin-blue)](https://docs.openclaw.ai)

An **OpenClaw Plugin + CLI Skill** for automated SonarQube code quality analysis with intelligent issue detection and suggested solutions.

## 🎯 Dual Mode

This package works in **two modes**:

| Mode | Use Case | Installation |
|------|----------|--------------|
| **OpenClaw Plugin** | AI agents, automation, cron jobs | Add to `openclaw.json` |
| **CLI Tool** | Manual analysis, CI/CD scripts | `npm install -g` or `npx` |

## 🎯 Overview

This skill integrates with SonarQube self-hosted instances to:
- Fetch and analyze code quality issues
- Provide intelligent solutions based on SonarQube rules
- Generate actionable reports in JSON or Markdown
- Check Quality Gate status
- Distinguish between auto-fixable and manual-fix issues

## 🚀 Quick Start

### Mode 1: OpenClaw Plugin (Recommended for AI Agents)

Install as an OpenClaw plugin to use native tools (`sonar_get_issues`, `sonar_analyze_and_suggest`, `sonar_quality_gate`):

```bash
# Clone to your OpenClaw workspace
git clone https://github.com/FelipeOFF/sonarqube-analyzer.git \
  ~/.openclaw/workspace/skills/sonarqube-analyzer

# Install dependencies
cd ~/.openclaw/workspace/skills/sonarqube-analyzer
npm install
```

Add to your `~/.openclaw/openclaw.json`:

```json
{
  "plugins": {
    "load": {
      "paths": [
        "/home/YOUR_USER/.openclaw/workspace/skills/sonarqube-analyzer"
      ]
    },
    "entries": {
      "sonarqube-analyzer": {
        "enabled": true,
        "config": {
          "sonarHostUrl": "http://127.0.0.1:9000",
          "sonarToken": "your-token-here",
          "defaultProject": "my-project"
        }
      }
    }
  }
}
```

Then reload OpenClaw:
```bash
pkill -USR1 -f "openclaw gateway"
```

**Available Tools:**
- `sonar_get_issues(projectKey, pullRequest?)` — Fetch issues from SonarQube
- `sonar_analyze_and_suggest(projectKey, pullRequest?, autoFix?)` — Analyze with solutions
- `sonar_quality_gate(projectKey, pullRequest?)` — Check Quality Gate status

### Mode 2: CLI Tool (For Manual Use & CI/CD)

```bash
# Install globally
npm install -g @felipeoff/sonarqube-analyzer

# Or use with npx (no install)
npx @felipeoff/sonarqube-analyzer --project=my-project
```

Configure environment variables:
```bash
export SONAR_HOST_URL=http://127.0.0.1:9000
export SONAR_TOKEN=your-token-here
```

### Basic Usage

```bash
# Analyze a project
node scripts/analyze.js --project=my-project

# Analyze a specific PR
node scripts/analyze.js --project=my-project --pr=5

# Generate Markdown report
node scripts/analyze.js --project=my-project --pr=5 --format=markdown

# Check Quality Gate status
node scripts/analyze.js --project=my-project --pr=5 --action=quality-gate
```

## 📋 Features

### Issue Analysis
- Fetches all open issues from SonarQube
- Groups by severity (BLOCKER, CRITICAL, MAJOR, MINOR, INFO)
- Provides detailed context (file, line, rule, message)

### Intelligent Solutions
The skill includes a comprehensive rule database with:
- **Problem description**: Clear explanation of the issue
- **Solution**: Step-by-step fix instructions
- **Code examples**: Before/after code snippets
- **Auto-fixable flag**: Indicates if the fix can be automated

### Supported Rules

| Rule ID | Issue | Auto-fixable |
|---------|-------|--------------|
| `typescript:S6606` | Prefer nullish coalescing (`??`) over logical OR (`\|\|`) | ✅ Yes |
| `typescript:S6749` | Redundant fragment with single child | ✅ Yes |
| `typescript:S6759` | Props should be marked as `readonly` | ✅ Yes |
| `typescript:S6571` | Redundant `any` in union types | ✅ Yes |
| `typescript:S3358` | Nested ternary operations | ❌ No (requires refactoring) |
| `typescript:S3776` | High cognitive complexity | ❌ No (requires extraction) |

### Report Generation

**JSON Format** (default):
```json
{
  "totalIssues": 12,
  "autoFixable": {
    "count": 8,
    "issues": [...]
  },
  "manualFix": {
    "count": 4,
    "issues": [...]
  },
  "nextSteps": [
    "Apply auto-fixable issues",
    "Refactor nested ternaries",
    "Run lint and typecheck"
  ]
}
```

**Markdown Format**:
```markdown
# SonarQube Analysis: my-project (PR #5)

**Total Issues:** 12

- 🔧 Auto-fixable: 8
- 📝 Manual fix required: 4

## Next Steps
1. Apply 8 auto-fixable issues
2. Refactor nested ternaries in App.tsx
3. Run lint and typecheck after fixes
```

## 🔧 Configuration

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SONAR_HOST_URL` | `http://127.0.0.1:9000` | SonarQube server URL |
| `SONAR_TOKEN` | `admin` | Authentication token |

### CLI Options

```
Options:
  --project=<name>    Project key in SonarQube (required)
  --pr=<number>       Pull request number (optional)
  --action=<type>     Action: analyze | report | quality-gate (default: analyze)
  --format=<type>     Output: json | markdown (default: json)
```

## 🔌 Architecture

```
sonarqube-analyzer/
├── scripts/
│   └── analyze.js          # CLI entry point
├── src/
│   ├── api.js              # SonarQube API client
│   ├── rules.js            # Rule definitions & solutions
│   ├── analyzer.js         # Issue analysis engine
│   └── reporter.js         # Report generators
├── tests/
│   └── *.test.js           # Unit tests
├── openclaw.plugin.json    # OpenClaw plugin manifest
├── SKILL.md                # OpenClaw skill documentation
├── README.md               # This file
└── package.json            # NPM manifest + OpenClaw config
```

**Dual-mode design:** The same codebase powers both the CLI tool and the OpenClaw plugin. The `openclaw.plugin.json` declares the tool schema, while `package.json` handles the CLI binary.

## 🧪 Testing

```bash
# Run all tests
npm test

# Run with coverage
npm run test:coverage

# Run specific test
npm test -- analyze.test.js
```

## 🤝 Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit changes (`git commit -m 'Add amazing feature'`)
4. Push to branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## 📝 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## 🙏 Acknowledgments

- Built for [OpenClaw](https://github.com/openclaw/openclaw)
- Inspired by SonarQube's code quality philosophy
- Thanks to the OpenClaw community for feedback and support

## 📞 Support

- **Issues**: [GitHub Issues](https://github.com/FelipeOFF/sonarqube-analyzer/issues)
- **Discussions**: [GitHub Discussions](https://github.com/FelipeOFF/sonarqube-analyzer/discussions)
- **OpenClaw Docs**: https://docs.openclaw.ai

---

**Made with ❤️ for the OpenClaw community**

path: openclaw.plugin.json
content: {
  "$schema": "https://schemas.openclaw.ai/plugin.json",
  "id": "sonarqube-analyzer",
  "name": "SonarQube Analyzer",
  "description": "Analyze SonarQube issues and suggest intelligent solutions for code quality improvement",
  "version": "1.0.0",
  "author": {
    "name": "FelipeOFF",
    "email": "[REDACTED_SECRET]",
    "url": "https://github.com/FelipeOFF"
  },
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/FelipeOFF/sonarqube-analyzer.git"
  },
  "bugs": {
    "url": "https://github.com/FelipeOFF/sonarqube-analyzer/issues"
  },
  "homepage": "https://github.com/FelipeOFF/sonarqube-analyzer#readme",
  "engines": {
    "openclaw": ">=1.0.0",
    "node": ">=18.0.0"
  },
  "categories": [
    "code-quality",
    "analysis",
    "ci-cd"
  ],
  "keywords": [
    "sonarqube",
    "code-quality",
    "static-analysis",
    "code-review",
    "linter"
  ],
  "tools": [
    {
      "name": "sonar_get_issues",
      "description": "Fetch issues from SonarQube for a project or pull request",
      "parameters": {
        "type": "object",
        "properties": {
          "projectKey": {
            "type": "string",
            "description": "SonarQube project key"
          },
          "pullRequest": {
            "type": "string",
            "description": "Pull request number (optional)"
          },
          "severities": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"]
            },
            "description": "Filter by severities"
          },
          "limit": {
            "type": "number",
            "default": 100,
            "description": "Maximum number of issues to return"
          }
        },
        "required": ["projectKey"]
      }
    },
    {
      "name": "sonar_analyze_and_suggest",
      "description": "Analyze issues and suggest solutions based on SonarQube rules",
      "parameters": {
        "type": "object",
        "properties": {
          "projectKey": {
            "type": "string",
            "description": "SonarQube project key"
          },
          "pullRequest": {
            "type": "string",
            "description": "Pull request number (optional)"
          },
          "autoFix": {
            "type": "boolean",
            "default": false,
            "description": "Attempt to apply automatic fixes"
          }
        },
        "required": ["projectKey"]
      }
    },
    {
      "name": "sonar_quality_gate",
      "description": "Check the Quality Gate status of a project",
      "parameters": {
        "type": "object",
        "properties": {
          "projectKey": {
            "type": "string",
            "description": "SonarQube project key"
          },
          "pullRequest": {
            "type": "string",
            "description": "Pull request number (optional)"
          }
        },
        "required": ["projectKey"]
      }
    }
  ],
  "config": {
    "schema": {
      "type": "object",
      "properties": {
        "sonarHostUrl": {
          "type": "string",
          "format": "uri",
          "default": "http://127.0.0.1:9000",
          "description": "URL of the SonarQube server"
        },
        "sonarToken": {
          "type": "string",
          "description": "Authentication token for SonarQube API"
        },
        "defaultProject": {
          "type": "string",
          "description": "Default project key to analyze when none is specified"
        }
      }
    }
  },
  "scripts": {
    "analyze": "node scripts/analyze.js"
  }
}

path: package.json
content: {
  "name": "@felipeoff/sonarqube-analyzer",
  "version": "1.0.0",
  "description": "OpenClaw skill for SonarQube code quality analysis with intelligent issue detection",
  "main": "src/index.js",
  "bin": {
    "sonarqube-analyzer": "./scripts/analyze.js"
  },
  "scripts": {
    "analyze": "node scripts/analyze.js",
    "test": "jest",
    "test:coverage": "jest --coverage",
    "lint": "eslint src scripts",
    "typecheck": "tsc --noEmit"
  },
  "keywords": [
    "openclaw",
    "openclaw-skill",
    "sonarqube",
    "code-quality",
    "static-analysis",
    "code-review",
    "ci-cd"
  ],
  "author": "FelipeOFF <[REDACTED_SECRET]>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/FelipeOFF/sonarqube-analyzer.git"
  },
  "bugs": {
    "url": "https://github.com/FelipeOFF/sonarqube-analyzer/issues"
  },
  "homepage": "https://github.com/FelipeOFF/sonarqube-analyzer#readme",
  "engines": {
    "node": ">=18.0.0"
  },
  "dependencies": {},
  "devDependencies": {
    "jest": "^29.0.0",
    "eslint": "^8.0.0"
  },
  "openclaw": {
    "skillId": "sonarqube-analyzer",
    "version": "1.0.0",
    "tools": [
      "sonar_get_issues",
      "sonar_analyze_and_suggest",
      "sonar_quality_gate"
    ],
    "config": {
      "schema": {
        "type": "object",
        "properties": {
          "sonarHostUrl": {
            "type": "string",
            "default": "http://127.0.0.1:9000",
            "description": "SonarQube server URL"
          },
          "sonarToken": {
            "type": "string",
            "description": "SonarQube authentication token"
          },
          "defaultProject": {
            "type": "string",
            "description": "Default project key to analyze"
          }
        }
      }
    }
  }
}

path: scripts/analyze.js
content: #!/usr/bin/env node
/**
 * SonarQube Analyzer CLI
 * 
 * CLI tool for analyzing SonarQube issues and suggesting solutions
 * 
 * Usage:
 *   node analyze.js --project=my-project [--pr=5] [--action=analyze|report|quality-gate] [--format=json|markdown]
 */

const { analyzeProject } = require('../src/analyzer');
const { getQualityGate } = require('../src/api');
const { generateJsonReport, generateMarkdownReport, generateCliSummary } = require('../src/reporter');

/**
 * Parse CLI arguments
 * @param {string[]} args - Process arguments
 * @returns {Object} Parsed options
 */
function parseArgs(args) {
  const params = {
    project: null,
    pr: null,
    action: 'analyze',
    format: 'json',
    severities: [],
    limit: 100
  };

  args.forEach(arg => {
    if (arg.startsWith('--project=')) {
      params.project = arg.split('=')[1];
    } else if (arg.startsWith('--pr=')) {
      params.pr = arg.split('=')[1];
    } else if (arg.startsWith('--action=')) {
      params.action = arg.split('=')[1];
    } else if (arg.startsWith('--format=')) {
      params.format = arg.split('=')[1];
    } else if (arg.startsWith('--limit=')) {
      params.limit = parseInt(arg.split('=')[1], 10);
    } else if (arg.startsWith('--severities=')) {
      params.severities = arg.split('=')[1].split(',');
    } else if (arg === '--help' || arg === '-h') {
      showHelp();
      process.exit(0);
    }
  });

  return params;
}

/**
 * Show help message
 */
function showHelp() {
  console.log(`
SonarQube Analyzer - CLI Tool

Usage: node analyze.js [options]

Options:
  --project=<name>      Project key in SonarQube (required)
  --pr=<number>         Pull request number (optional)
  --action=<type>       Action to perform:
                         - analyze: Full analysis with suggestions (default)
                         - quality-gate: Check Quality Gate status only
  --format=<type>       Output format: json | markdown | cli (default: json)
  --limit=<number>      Maximum issues to fetch (default: 100)
  --severities=<list>   Comma-separated severity filters
  --help, -h             Show this help message

Environment Variables:
  SONAR_HOST_URL         SonarQube URL (default: http://127.0.0.1:9000)
  SONAR_TOKEN            Authentication token (default: admin)

Examples:
  # Analyze project
  node analyze.js --project=my-project

  # Analyze PR with markdown output
  node analyze.js --project=my-project --pr=5 --format=markdown

  # Check Quality Gate
  node analyze.js --project=my-project --pr=5 --action=quality-gate

  # Filter by severity
  node analyze.js --project=my-project --severities=CRITICAL,MAJOR
`);
}

/**
 * Main execution
 */
async function main() {
  const args = process.argv.slice(2);
  const params = parseArgs(args);

  if (!params.project) {
    console.error('❌ Error: --project is required');
    console.log('\nRun with --help for usage information\n');
    process.exit(1);
  }

  try {
    switch (params.action) {
      case 'analyze': {
        console.log(`🔍 Analyzing ${params.project}${params.pr ? ` (PR #${params.pr})` : ''}...\n`);
        
        const analysis = await analyzeProject(params.project, params.pr, {
          severities: params.severities,
          limit: params.limit
        });

        let output;
        switch (params.format) {
          case 'markdown':
            output = generateMarkdownReport(analysis);
            break;
          case 'cli':
            output = generateCliSummary(analysis);
            break;
          case 'json':
          default:
            output = generateJsonReport(analysis);
            break;
        }

        console.log(output);
        
        // Exit with error code if issues found
        if (analysis.summary.totalIssues > 0) {
          process.exit(1);
        }
        break;
      }

      case 'quality-gate': {
        console.log(`🚪 Checking Quality Gate for ${params.project}${params.pr ? ` (PR #${params.pr})` : ''}...\n`);
        
        const qg = await getQualityGate(params.project, params.pr);
        
        console.log(JSON.stringify(qg, null, 2));
        
        if (qg.status === 'ERROR' || qg.status === 'FAILED') {
          console.log('\n❌ Quality Gate failed');
          process.exit(1);
        } else if (qg.status === 'OK' || qg.status === 'PASSED') {
          console.log('\n✅ Quality Gate passed');
        }
        break;
      }

      default:
        console.error(`❌ Unknown action: ${params.action}`);
        process.exit(1);
    }
  } catch (error) {
    console.error(`\n❌ Error: ${error.message}`);
    process.exit(1);
  }
}

// Run if executed directly
if (require.main === module) {
  main();
}

module.exports = { parseArgs, showHelp };

path: src/analyzer.js
content: /**
 * Issue Analyzer
 * Orchestrates fetching and analyzing SonarQube issues
 */

const { getIssues } = require('./api');
const { analyzeIssue } = require('./rules');

/**
 * Generate suggestions for fixing issues
 * @param {Object[]} issues - List of issues
 * @returns {Object} Analysis results
 */
function generateSuggestions(issues) {
  const analyzed = issues.map(analyzeIssue);
  
  const autoFixable = analyzed.filter(i => i.autoFixable);
  const manualFix = analyzed.filter(i => !i.autoFixable);

  const byFile = analyzed.reduce((acc, issue) => {
    acc[issue.component] = acc[issue.component] || [];
    acc[issue.component].push(issue);
    return acc;
  }, {});

  return {
    totalIssues: analyzed.length,
    autoFixable: {
      count: autoFixable.length,
      issues: autoFixable
    },
    manualFix: {
      count: manualFix.length,
      issues: manualFix
    },
    byFile,
    nextSteps: generateNextSteps(analyzed)
  };
}

/**
 * Generate next steps based on issues
 * @param {Object[]} analyzed - Analyzed issues
 * @returns {string[]} List of next steps
 */
function generateNextSteps(analyzed) {
  const steps = [];
  
  const hasAutoFix = analyzed.some(i => i.autoFixable);
  const hasManualFix = analyzed.some(i => !i.autoFixable);
  const filesAffected = new Set(analyzed.map(i => i.component)).size;

  if (hasAutoFix) {
    const count = analyzed.filter(i => i.autoFixable).length;
    steps.push(`Apply ${count} auto-fixable issues (ESLint/Prettier can help)`);
  }

  if (hasManualFix) {
    const count = analyzed.filter(i => !i.autoFixable).length;
    steps.push(`Refactor ${count} issues requiring manual changes`);
  }

  steps.push(`Review changes in ${filesAffected} file(s)`);
  steps.push('Run lint and typecheck after fixes');
  steps.push('Commit with message: "fix: resolve SonarQube issues"');

  return steps;
}

/**
 * Analyze a project and generate full report
 * @param {string} projectKey - Project key
 * @param {string} [pullRequest] - PR number
 * @param {Object} [options] - Analysis options
 * @returns {Promise<Object>} Full analysis report
 */
async function analyzeProject(projectKey, pullRequest, options = {}) {
  const { severities = [], limit = 100 } = options;
  
  const { total, issues } = await getIssues(projectKey, pullRequest, severities, limit);
  const analysis = generateSuggestions(issues);
  
  return {
    projectKey,
    pullRequest,
    timestamp: new Date().toISOString(),
    summary: {
      total,
      ...analysis
    }
  };
}

module.exports = {
  generateSuggestions,
  generateNextSteps,
  analyzeProject
};

path: src/api.js
content: /**
 * SonarQube API Client
 * Handles communication with SonarQube server
 */

const SONAR_HOST = process.env.SONAR_HOST_URL || 'http://127.0.0.1:9000';
const SONAR_TOKEN = process.env.SONAR_TOKEN || 'admin';

/**
 * Make a request to SonarQube API
 * @param {string} endpoint - API endpoint (without /api prefix)
 * @param {Object} params - Query parameters
 * @returns {Promise<Object>} API response
 */
async function fetchFromSonarQube(endpoint, params = {}) {
  const url = new URL(`${SONAR_HOST}/api${endpoint}`);
  
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined && value !== null) {
      url.searchParams.append(key, value);
    }
  });

  const headers = {};
  
  // Only add auth if token is provided and not default
  if (SONAR_TOKEN && SONAR_TOKEN !== 'admin') {
    headers['Authorization'] = `Basic ${Buffer.from(`${SONAR_TOKEN}:`).toString('base64')}`;
  }

  const response = await fetch(url.toString(), { headers });

  if (!response.ok) {
    throw new Error(`SonarQube API error: ${response.status} ${response.statusText}`);
  }

  return response.json();
}

/**
 * Fetch issues from SonarQube
 * @param {string} projectKey - Project key
 * @param {string} [pullRequest] - Pull request number
 * @param {string[]} [severities] - Severity filters
 * @param {number} [limit=100] - Maximum issues to fetch
 * @returns {Promise<Object>} Issues data
 */
async function getIssues(projectKey, pullRequest, severities = [], limit = 100) {
  const params = {
    componentKeys: projectKey,
    ps: limit,
    resolved: 'false'
  };

  if (pullRequest) {
    params.pullRequest = pullRequest;
  }

  if (severities.length > 0) {
    params.severities = severities.join(',');
  }

  const data = await fetchFromSonarQube('/issues/search', params);
  
  return {
    total: data.total,
    issues: data.issues.map(issue => ({
      key: issue.key,
      severity: issue.severity,
      component: issue.component.split(':').pop(),
      line: issue.line,
      message: issue.message,
      rule: issue.rule,
      status: issue.status,
      effort: issue.effort,
      type: issue.type
    }))
  };
}

/**
 * Check Quality Gate status
 * @param {string} projectKey - Project key
 * @param {string} [pullRequest] - Pull request number
 * @returns {Promise<Object>} Quality gate status
 */
async function getQualityGate(projectKey, pullRequest) {
  const params = { projectKey };

  if (pullRequest) {
    params.pullRequest = pullRequest;
  }

  try {
    const data = await fetchFromSonarQube('/qualitygates/project_status', params);
    return {
      status: data.projectStatus.status,
      conditions: data.projectStatus.conditions || []
    };
  } catch (error) {
    return {
      status: 'UNKNOWN',
      error: error.message
    };
  }
}

module.exports = {
  fetchFromSonarQube,
  getIssues,
  getQualityGate
};

path: src/index.js
content: /**
 * SonarQube Analyzer - Main Entry Point
 * OpenClaw skill for automated code quality analysis
 */

const api = require('./api');
const rules = require('./rules');
const analyzer = require('./analyzer');
const reporter = require('./reporter');

// Export all modules
module.exports = {
  // API client
  ...api,
  
  // Rule definitions
  ...rules,
  
  // Analysis engine
  ...analyzer,
  
  // Report generators
  ...reporter
};

path: src/reporter.js
content: /**
 * Report Generators
 * Generates human-readable reports in various formats
 */

/**
 * Generate JSON report
 * @param {Object} analysis - Analysis results
 * @returns {string} JSON string
 */
function generateJsonReport(analysis) {
  return JSON.stringify(analysis, null, 2);
}

/**
 * Generate Markdown report
 * @param {Object} analysis - Analysis results
 * @returns {string} Markdown content
 */
function generateMarkdownReport(analysis) {
  const { projectKey, pullRequest, timestamp, summary } = analysis;
  
  let md = `# SonarQube Analysis Report\n\n`;
  md += `**Project:** ${projectKey}\n`;
  if (pullRequest) {
    md += `**Pull Request:** #${pullRequest}\n`;
  }
  md += `**Generated:** ${new Date(timestamp).toLocaleString()}\n\n`;
  
  // Summary
  md += `## Summary\n\n`;
  md += `- **Total Issues:** ${summary.totalIssues}\n`;
  md += `- 🔧 Auto-fixable: ${summary.autoFixable.count}\n`;
  md += `- 📝 Manual fix required: ${summary.manualFix.count}\n\n`;
  
  // Quality indicator
  const total = summary.totalIssues;
  const autoFixable = summary.autoFixable.count;
  const ratio = total > 0 ? (autoFixable / total) * 100 : 0;
  
  if (ratio >= 70) {
    md += `✅ **Good news:** ${ratio.toFixed(0)}% of issues can be fixed automatically!\n\n`;
  } else if (ratio >= 40) {
    md += `⚠️ **Mixed:** ${ratio.toFixed(0)}% of issues are auto-fixable. Some manual work required.\n\n`;
  } else if (total > 0) {
    md += `🔧 **Attention needed:** Most issues require manual refactoring.\n\n`;
  }
  
  // Next steps
  md += `## Recommended Next Steps\n\n`;
  summary.nextSteps.forEach((step, i) => {
    md += `${i + 1}. ${step}\n`;
  });
  md += '\n';
  
  // Auto-fixable issues
  if (summary.autoFixable.count > 0) {
    md += `## Auto-fixable Issues\n\n`;
    md += `These issues can be fixed automatically with ESLint, Prettier, or simple replacements:\n\n`;
    
    summary.autoFixable.issues.forEach(issue => {
      md += `### ${issue.component}:${issue.line}\n`;
      md += `- **Rule:** ${issue.ruleTitle} (${issue.rule})\n`;
      md += `- **Severity:** ${issue.severity}\n`;
      md += `- **Solution:** ${issue.solution}\n`;
      if (issue.example) {
        md += `- **Example:**\n\n${issue.example}\n`;
      }
      md += '\n';
    });
  }
  
  // Manual fix issues
  if (summary.manualFix.count > 0) {
    md += `## Manual Fix Required\n\n`;
    md += `These issues require manual refactoring and code review:\n\n`;
    
    summary.manualFix.issues.forEach(issue => {
      md += `### ${issue.component}:${issue.line} [${issue.severity}]\n`;
      md += `- **Issue:** ${issue.message}\n`;
      md += `- **Rule:** ${issue.ruleTitle} (${issue.rule})\n`;
      md += `- **Solution:** ${issue.solution}\n`;
      if (issue.example) {
        md += `- **Example:**\n\n${issue.example}\n`;
      }
      md += '\n';
    });
  }
  
  // Files affected
  md += `## Files Affected\n\n`;
  Object.entries(summary.byFile).forEach(([file, issues]) => {
    md += `- **${file}**: ${issues.length} issue(s)\n`;
  });
  md += '\n';
  
  // Footer
  md += `---\n`;
  md += `*Generated by [sonarqube-analyzer](https://github.com/FelipeOFF/sonarqube-analyzer)*\n`;
  
  return md;
}

/**
 * Generate CLI-friendly summary
 * @param {Object} analysis - Analysis results
 * @returns {string} CLI output
 */
function generateCliSummary(analysis) {
  const { summary } = analysis;
  
  let output = '';
  output += `╔════════════════════════════════════════════════╗\n`;
  output += `║     SonarQube Analysis Summary                 ║\n`;
  output += `╚════════════════════════════════════════════════╝\n\n`;
  output += `Total Issues: ${summary.totalIssues}\n`;
  output += `  🔧 Auto-fixable: ${summary.autoFixable.count}\n`;
  output += `  📝 Manual fix: ${summary.manualFix.count}\n\n`;
  
  if (summary.nextSteps.length > 0) {
    output += 'Next Steps:\n';
    summary.nextSteps.forEach((step, i) => {
      output += `  ${i + 1}. ${step}\n`;
    });
  }
  
  return output;
}

module.exports = {
  generateJsonReport,
  generateMarkdownReport,
  generateCliSummary
};

path: src/rules.js
content: /**
 * SonarQube Rule Definitions and Solutions
 * Database of known rules with automated solutions
 */

/**
 * Rule solutions database
 * Each rule contains:
 * - title: Human-readable rule name
 * - description: Detailed explanation
 * - autoFixable: Whether the fix can be automated
 * - solution: Function that returns solution text
 * - example: Code example showing before/after
 */
const RULE_SOLUTIONS = {
  // TypeScript Rules
  'typescript:S3358': {
    title: 'Nested ternary operation',
    description: 'Extract nested ternary into an independent statement or component',
    autoFixable: false,
    solution: (issue) => `Extract the nested ternary at line ${issue.line} into a separate function or component.`,
    example: `
// ❌ Before (nested ternary):
{loading ? 'Loading...' : runs.length > 0 ? <Table /> : <p>No data</p>}

// ✅ After (extracted component):
function DataContent({ loading, runs }) {
  if (loading) return 'Loading...';
  if (runs.length === 0) return <p>No data</p>;
  return <Table />;
}
    `
  },

  'typescript:S6606': {
    title: 'Prefer nullish coalescing operator',
    description: 'Use ?? instead of || for default values to avoid bugs with falsy values',
    autoFixable: true,
    solution: (issue) => `Replace || with ?? for default value assignment.`,
    example: `
// ❌ Before:
const name = value || 'default';
const count = input || 0;

// ✅ After:
const name = value ?? 'default';
const count = input ?? 0;
    `
  },

  'typescript:S6749': {
    title: 'Redundant fragment',
    description: 'Fragment with only one child is redundant and should be removed',
    autoFixable: true,
    solution: (issue) => `Remove the unnecessary <>...</> fragment.`,
    example: `
// ❌ Before:
<><div>Content</div></>

// ✅ After:
<div>Content</div>
    `
  },

  'typescript:S6759': {
    title: 'Props should be marked as read-only',
    description: 'Component props should be marked as readonly to prevent mutations',
    autoFixable: true,
    solution: (issue) => `Add 'readonly' modifier to props type definition.`,
    example: `
// ❌ Before:
function Component({ name }: { name: string })
interface Props { name: string }

// ✅ After:
function Component({ name }: { readonly name: string })
interface Props { readonly name: string }
    `
  },

  'typescript:S3776': {
    title: 'Cognitive complexity too high',
    description: 'Function has too high cognitive complexity and should be refactored',
    autoFixable: false,
    solution: (issue) => `Extract parts of the function into smaller, focused components or functions.`,
    example: `
// Strategies to reduce complexity:
// 1. Extract logic into custom hooks
// 2. Move UI sections to separate components  
// 3. Use early returns to reduce nesting
// 4. Replace conditionals with lookup objects
    `
  },

  'typescript:S6571': {
    title: 'Redundant any in union type',
    description: 'any type already includes all types, making the union redundant',
    autoFixable: true,
    solution: (issue) => `Remove redundant union with 'any' - 'any' already includes all types.`,
    example: `
// ❌ Before:
const [data, setData] = useState<any | null>(null);
const value: string | any;

// ✅ After:
const [data, setData] = useState<any>(null);
const value: any;
    `
  },

  // JavaScript Rules
  'javascript:S3358': {
    title: 'Nested ternary operation',
    description: 'Extract nested ternary into an independent statement',
    autoFixable: false,
    solution: (issue) => `Extract the nested ternary at line ${issue.line} into a separate function.`,
    example: `
// ❌ Before:
const result = condition1 ? (condition2 ? 'a' : 'b') : 'c';

// ✅ After:
function getResult(condition1, condition2) {
  if (!condition1) return 'c';
  return condition2 ? 'a' : 'b';
}
    `
  },

  // Add more rules as needed
};

/**
 * Get solution for a specific rule
 * @param {string} ruleKey - SonarQube rule key
 * @returns {Object|null} Rule solution or null if not found
 */
function getRuleSolution(ruleKey) {
  return RULE_SOLUTIONS[ruleKey] || null;
}

/**
 * Check if a rule has an automated fix
 * @param {string} ruleKey - SonarQube rule key
 * @returns {boolean} True if auto-fixable
 */
function isAutoFixable(ruleKey) {
  const rule = RULE_SOLUTIONS[ruleKey];
  return rule ? rule.autoFixable : false;
}

/**
 * Analyze an issue and add solution information
 * @param {Object} issue - SonarQube issue
 * @returns {Object} Issue with added solution data
 */
function analyzeIssue(issue) {
  const ruleInfo = getRuleSolution(issue.rule);
  
  if (!ruleInfo) {
    return {
      ...issue,
      ruleTitle: 'Unknown Rule',
      ruleDescription: 'No automated solution available. Check SonarQube documentation.',
      solution: 'Check SonarQube documentation for this rule.',
      autoFixable: false,
      action: 'manual'
    };
  }

  return {
    ...issue,
    ruleTitle: ruleInfo.title,
    ruleDescription: ruleInfo.description,
    solution: ruleInfo.solution(issue),
    autoFixable: ruleInfo.autoFixable,
    example: ruleInfo.example,
    action: ruleInfo.autoFixable ? 'auto-fix' : 'manual-refactor'
  };
}

module.exports = {
  RULE_SOLUTIONS,
  getRuleSolution,
  isAutoFixable,
  analyzeIssue
};

Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (HIGH)