opencode-sonarqube 2.1.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -2
- package/dist/index.js +365 -82
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -212,7 +212,7 @@ Create `.sonarqube/config.json` in your project root:
|
|
|
212
212
|
}
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
-
## Tool Actions (
|
|
215
|
+
## Tool Actions (16 total)
|
|
216
216
|
|
|
217
217
|
The plugin adds a `sonarqube` tool with these actions:
|
|
218
218
|
|
|
@@ -462,6 +462,152 @@ This will:
|
|
|
462
462
|
|
|
463
463
|
**Note:** The `.sonarqube/` directory contains sensitive tokens - never commit it!
|
|
464
464
|
|
|
465
|
+
## Troubleshooting
|
|
466
|
+
|
|
467
|
+
### Common Issues
|
|
468
|
+
|
|
469
|
+
#### "Cannot connect to SonarQube server"
|
|
470
|
+
|
|
471
|
+
**Symptoms:** Plugin shows connection errors, health check fails.
|
|
472
|
+
|
|
473
|
+
**Solutions:**
|
|
474
|
+
1. **Check URL format**: Must include protocol (`https://` or `http://`)
|
|
475
|
+
```bash
|
|
476
|
+
# Wrong
|
|
477
|
+
export SONAR_HOST_URL="sonarqube.example.com"
|
|
478
|
+
# Correct
|
|
479
|
+
export SONAR_HOST_URL="https://sonarqube.example.com"
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
2. **Verify server is running**:
|
|
483
|
+
```bash
|
|
484
|
+
curl -s "$SONAR_HOST_URL/api/system/status" | jq .status
|
|
485
|
+
# Should output: "UP"
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
3. **Check firewall/VPN**: Ensure your machine can reach the server.
|
|
489
|
+
|
|
490
|
+
4. **Verify credentials**:
|
|
491
|
+
```bash
|
|
492
|
+
curl -u "$SONAR_USER:$SONAR_PASSWORD" "$SONAR_HOST_URL/api/authentication/validate"
|
|
493
|
+
# Should output: {"valid":true}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
#### "Authentication failed" / 401 Errors
|
|
497
|
+
|
|
498
|
+
**Solutions:**
|
|
499
|
+
1. **Reload environment variables**:
|
|
500
|
+
```bash
|
|
501
|
+
source ~/.zshrc # or ~/.bashrc
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
2. **Check credentials are set**:
|
|
505
|
+
```bash
|
|
506
|
+
echo $SONAR_HOST_URL # Should show your URL
|
|
507
|
+
echo $SONAR_USER # Should show username
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
3. **Verify password has no special characters** that need escaping:
|
|
511
|
+
```bash
|
|
512
|
+
# If password contains special chars, quote it:
|
|
513
|
+
export SONAR_PASSWORD='my$pecial!pass'
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
#### "Project not found" after setup
|
|
517
|
+
|
|
518
|
+
**Solutions:**
|
|
519
|
+
1. **Re-run setup with force**:
|
|
520
|
+
```typescript
|
|
521
|
+
sonarqube({ action: "setup", force: true })
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
2. **Check if project exists on server**: Visit `$SONAR_HOST_URL/projects` in browser.
|
|
525
|
+
|
|
526
|
+
3. **Delete local state and re-initialize**:
|
|
527
|
+
```bash
|
|
528
|
+
rm -rf .sonarqube/
|
|
529
|
+
# Then in OpenCode: sonarqube({ action: "setup" })
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Plugin doesn't load / "Plugin not found"
|
|
533
|
+
|
|
534
|
+
**Solutions:**
|
|
535
|
+
1. **Verify installation**:
|
|
536
|
+
```bash
|
|
537
|
+
cat package.json | grep opencode-sonarqube
|
|
538
|
+
# Should show: "opencode-sonarqube": "..."
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
2. **Check opencode.json**:
|
|
542
|
+
```json
|
|
543
|
+
{
|
|
544
|
+
"plugin": ["opencode-sonarqube"]
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
3. **Reinstall**:
|
|
549
|
+
```bash
|
|
550
|
+
bun remove opencode-sonarqube && bun add opencode-sonarqube
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
4. **Restart OpenCode** after installation.
|
|
554
|
+
|
|
555
|
+
#### Analysis shows no issues but Quality Gate fails
|
|
556
|
+
|
|
557
|
+
This usually means **security hotspots** need review.
|
|
558
|
+
|
|
559
|
+
**Solution:**
|
|
560
|
+
```typescript
|
|
561
|
+
// List hotspots
|
|
562
|
+
sonarqube({ action: "hotspots" })
|
|
563
|
+
|
|
564
|
+
// Bulk-review as safe (if appropriate)
|
|
565
|
+
sonarqube({ action: "reviewhotspot", resolution: "SAFE" })
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
#### Slow analysis / Timeout errors
|
|
569
|
+
|
|
570
|
+
**Solutions:**
|
|
571
|
+
1. **Exclude unnecessary files** in `.sonarqube/config.json`:
|
|
572
|
+
```json
|
|
573
|
+
{
|
|
574
|
+
"exclusions": "**/node_modules/**,**/dist/**,**/*.min.js"
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
2. **Increase timeout** (set environment variable):
|
|
579
|
+
```bash
|
|
580
|
+
export SONARQUBE_TIMEOUT=120000 # 2 minutes
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
3. **Check server resources**: Analysis is CPU-intensive on the server side.
|
|
584
|
+
|
|
585
|
+
### Debug Mode
|
|
586
|
+
|
|
587
|
+
Enable detailed logging:
|
|
588
|
+
```bash
|
|
589
|
+
export SONARQUBE_DEBUG=true
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Logs are written to: `/tmp/sonarqube-plugin-debug.log`
|
|
593
|
+
|
|
594
|
+
View logs in real-time:
|
|
595
|
+
```bash
|
|
596
|
+
tail -f /tmp/sonarqube-plugin-debug.log
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Getting Help
|
|
600
|
+
|
|
601
|
+
1. **Check existing issues**: [GitHub Issues](https://github.com/mguttmann/opencode-sonarqube/issues)
|
|
602
|
+
2. **Open new issue** with:
|
|
603
|
+
- OpenCode version
|
|
604
|
+
- Plugin version (`bun list opencode-sonarqube`)
|
|
605
|
+
- SonarQube server version
|
|
606
|
+
- Error message/logs
|
|
607
|
+
- Steps to reproduce
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
465
611
|
## FAQ
|
|
466
612
|
|
|
467
613
|
### Where is the configuration stored?
|
|
@@ -505,7 +651,79 @@ sonarqube({ action: "analyze" })
|
|
|
505
651
|
|
|
506
652
|
### How do I use this with multiple SonarQube servers?
|
|
507
653
|
|
|
508
|
-
Currently, the plugin uses global environment variables. For different servers per project
|
|
654
|
+
Currently, the plugin uses global environment variables. For different servers per project:
|
|
655
|
+
|
|
656
|
+
**Option 1: Use direnv (recommended)**
|
|
657
|
+
```bash
|
|
658
|
+
# Install direnv
|
|
659
|
+
brew install direnv # macOS
|
|
660
|
+
|
|
661
|
+
# In project directory, create .envrc
|
|
662
|
+
echo 'export SONAR_HOST_URL="https://server1.example.com"' > .envrc
|
|
663
|
+
echo 'export SONAR_USER="admin"' >> .envrc
|
|
664
|
+
echo 'export SONAR_PASSWORD="password1"' >> .envrc
|
|
665
|
+
direnv allow
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Option 2: Use project-specific shell scripts**
|
|
669
|
+
```bash
|
|
670
|
+
# Create project setup script
|
|
671
|
+
cat > .sonarqube-env.sh << 'EOF'
|
|
672
|
+
export SONAR_HOST_URL="https://server1.example.com"
|
|
673
|
+
export SONAR_USER="admin"
|
|
674
|
+
export SONAR_PASSWORD="password1"
|
|
675
|
+
EOF
|
|
676
|
+
|
|
677
|
+
# Source before starting OpenCode
|
|
678
|
+
source .sonarqube-env.sh && opencode
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Option 3: Different terminal profiles**
|
|
682
|
+
Create shell profiles for each server and switch between them.
|
|
683
|
+
|
|
684
|
+
### What happens if SonarQube server is offline?
|
|
685
|
+
|
|
686
|
+
The plugin handles offline scenarios gracefully:
|
|
687
|
+
|
|
688
|
+
1. **During startup**: System prompt injection is skipped, no errors shown
|
|
689
|
+
2. **During analysis**: Error message explains the connection issue
|
|
690
|
+
3. **Cached data**: Last analysis results are preserved locally in `.sonarqube/project.json`
|
|
691
|
+
4. **Auto-retry**: The plugin automatically retries failed requests up to 3 times with exponential backoff
|
|
692
|
+
|
|
693
|
+
You can continue coding - the plugin will reconnect when the server is available again.
|
|
694
|
+
|
|
695
|
+
### How do I reset everything and start fresh?
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
# Remove local plugin state
|
|
699
|
+
rm -rf .sonarqube/
|
|
700
|
+
|
|
701
|
+
# Remove from package.json (optional)
|
|
702
|
+
bun remove opencode-sonarqube
|
|
703
|
+
|
|
704
|
+
# Reinstall
|
|
705
|
+
bun add opencode-sonarqube
|
|
706
|
+
|
|
707
|
+
# Restart OpenCode and run setup
|
|
708
|
+
# In OpenCode: sonarqube({ action: "setup" })
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Can I use a token instead of username/password?
|
|
712
|
+
|
|
713
|
+
Yes! SonarQube tokens are recommended for better security:
|
|
714
|
+
|
|
715
|
+
1. **Generate token** in SonarQube: User > My Account > Security > Generate Token
|
|
716
|
+
2. **Use as password** (leave user as your username):
|
|
717
|
+
```bash
|
|
718
|
+
export SONAR_USER="your-username"
|
|
719
|
+
export SONAR_PASSWORD="sqp_your_token_here"
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
Or use token directly (user becomes empty):
|
|
723
|
+
```bash
|
|
724
|
+
export SONAR_USER=""
|
|
725
|
+
export SONAR_PASSWORD="sqp_your_token_here"
|
|
726
|
+
```
|
|
509
727
|
|
|
510
728
|
### Can I use this without OpenCode?
|
|
511
729
|
|
package/dist/index.js
CHANGED
|
@@ -4743,13 +4743,79 @@ function buildAuthHeader(auth) {
|
|
|
4743
4743
|
const credentials = auth.user + ":" + auth.password;
|
|
4744
4744
|
return `Basic ${btoa(credentials)}`;
|
|
4745
4745
|
}
|
|
4746
|
+
function sleep(ms) {
|
|
4747
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4748
|
+
}
|
|
4749
|
+
function calculateBackoffDelay(attempt, baseDelay) {
|
|
4750
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
4751
|
+
const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
|
|
4752
|
+
return Math.min(exponentialDelay + jitter, 30000);
|
|
4753
|
+
}
|
|
4754
|
+
function isRetryableError(error45, statusCode) {
|
|
4755
|
+
if (error45 instanceof TypeError && error45.message.includes("fetch")) {
|
|
4756
|
+
return true;
|
|
4757
|
+
}
|
|
4758
|
+
if (statusCode && RETRYABLE_STATUS_CODES.has(statusCode)) {
|
|
4759
|
+
return true;
|
|
4760
|
+
}
|
|
4761
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
4762
|
+
const errorCode = error45.code;
|
|
4763
|
+
return errorCode === "CONNECTION_ERROR";
|
|
4764
|
+
}
|
|
4765
|
+
return false;
|
|
4766
|
+
}
|
|
4767
|
+
function getRateLimitWaitTime(response, attempt, baseDelay) {
|
|
4768
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
4769
|
+
return retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : calculateBackoffDelay(attempt, baseDelay);
|
|
4770
|
+
}
|
|
4746
4771
|
|
|
4747
4772
|
class SonarQubeClient {
|
|
4748
4773
|
baseUrl;
|
|
4749
4774
|
auth;
|
|
4775
|
+
maxRetries;
|
|
4776
|
+
retryDelay;
|
|
4777
|
+
timeout;
|
|
4750
4778
|
constructor(config3, _logger) {
|
|
4751
4779
|
this.baseUrl = normalizeUrl(config3.url);
|
|
4752
4780
|
this.auth = config3.auth;
|
|
4781
|
+
this.maxRetries = config3.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
4782
|
+
this.retryDelay = config3.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
4783
|
+
this.timeout = config3.timeout ?? DEFAULT_TIMEOUT;
|
|
4784
|
+
}
|
|
4785
|
+
async executeRequest(url2, method, headers, body, attempt) {
|
|
4786
|
+
const controller = new AbortController;
|
|
4787
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
4788
|
+
try {
|
|
4789
|
+
const response = await fetch(url2, { method, headers, body, signal: controller.signal });
|
|
4790
|
+
clearTimeout(timeoutId);
|
|
4791
|
+
return this.processResponse(response, attempt);
|
|
4792
|
+
} catch (error45) {
|
|
4793
|
+
clearTimeout(timeoutId);
|
|
4794
|
+
return this.handleFetchException(error45);
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
async processResponse(response, attempt) {
|
|
4798
|
+
if (response.ok) {
|
|
4799
|
+
const text = await response.text();
|
|
4800
|
+
return { success: true, data: parseResponseBody(text) };
|
|
4801
|
+
}
|
|
4802
|
+
const canRetry = attempt < this.maxRetries && RETRYABLE_STATUS_CODES.has(response.status);
|
|
4803
|
+
if (!canRetry) {
|
|
4804
|
+
await handleResponseError(response);
|
|
4805
|
+
throw new Error("Unreachable");
|
|
4806
|
+
}
|
|
4807
|
+
const waitTime = response.status === 429 ? getRateLimitWaitTime(response, attempt, this.retryDelay) : calculateBackoffDelay(attempt, this.retryDelay);
|
|
4808
|
+
return { success: false, shouldRetry: true, statusCode: response.status, waitTime };
|
|
4809
|
+
}
|
|
4810
|
+
handleFetchException(error45) {
|
|
4811
|
+
if (error45 instanceof Error && error45.name === "AbortError") {
|
|
4812
|
+
return { success: false, shouldRetry: false, error: error45 };
|
|
4813
|
+
}
|
|
4814
|
+
return {
|
|
4815
|
+
success: false,
|
|
4816
|
+
shouldRetry: isRetryableError(error45),
|
|
4817
|
+
error: error45
|
|
4818
|
+
};
|
|
4753
4819
|
}
|
|
4754
4820
|
async request(endpoint, options = {}) {
|
|
4755
4821
|
const { method = "GET", params, body } = options;
|
|
@@ -4762,20 +4828,23 @@ class SonarQubeClient {
|
|
|
4762
4828
|
if (requestBody) {
|
|
4763
4829
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
4764
4830
|
}
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
});
|
|
4771
|
-
if (!response.ok) {
|
|
4772
|
-
await handleResponseError(response);
|
|
4831
|
+
let lastError;
|
|
4832
|
+
for (let attempt = 0;attempt <= this.maxRetries; attempt++) {
|
|
4833
|
+
const result = await this.executeRequest(url2, method, headers, requestBody, attempt);
|
|
4834
|
+
if (result.success) {
|
|
4835
|
+
return result.data;
|
|
4773
4836
|
}
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4837
|
+
lastError = result.error;
|
|
4838
|
+
if (result.shouldRetry && attempt < this.maxRetries) {
|
|
4839
|
+
await sleep(result.waitTime ?? calculateBackoffDelay(attempt, this.retryDelay));
|
|
4840
|
+
continue;
|
|
4841
|
+
}
|
|
4842
|
+
if (result.error instanceof Error && result.error.name === "AbortError") {
|
|
4843
|
+
throw ConnectionError(`Request to ${endpoint} timed out after ${this.timeout}ms`);
|
|
4844
|
+
}
|
|
4845
|
+
handleFetchError(result.error, this.baseUrl);
|
|
4778
4846
|
}
|
|
4847
|
+
handleFetchError(lastError, this.baseUrl);
|
|
4779
4848
|
}
|
|
4780
4849
|
getBaseUrl() {
|
|
4781
4850
|
return this.baseUrl;
|
|
@@ -4834,14 +4903,16 @@ class SonarQubeClient {
|
|
|
4834
4903
|
return this.request(endpoint, { method: "DELETE", params });
|
|
4835
4904
|
}
|
|
4836
4905
|
}
|
|
4837
|
-
function createClientWithToken(url2, token, logger4) {
|
|
4838
|
-
return new SonarQubeClient({ url: url2, auth: { token } }, logger4);
|
|
4906
|
+
function createClientWithToken(url2, token, logger4, options) {
|
|
4907
|
+
return new SonarQubeClient({ url: url2, auth: { token }, ...options }, logger4);
|
|
4839
4908
|
}
|
|
4840
|
-
function createClientWithCredentials(url2, user, password, logger4) {
|
|
4841
|
-
return new SonarQubeClient({ url: url2, auth: { user, password } }, logger4);
|
|
4909
|
+
function createClientWithCredentials(url2, user, password, logger4, options) {
|
|
4910
|
+
return new SonarQubeClient({ url: url2, auth: { user, password }, ...options }, logger4);
|
|
4842
4911
|
}
|
|
4912
|
+
var DEFAULT_MAX_RETRIES = 3, DEFAULT_RETRY_DELAY = 1000, DEFAULT_TIMEOUT = 30000, RETRYABLE_STATUS_CODES;
|
|
4843
4913
|
var init_client = __esm(() => {
|
|
4844
4914
|
init_types2();
|
|
4915
|
+
RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
4845
4916
|
});
|
|
4846
4917
|
|
|
4847
4918
|
// src/api/projects.ts
|
|
@@ -4956,7 +5027,8 @@ __export(exports_state, {
|
|
|
4956
5027
|
hasProjectState: () => hasProjectState,
|
|
4957
5028
|
ensureGitignore: () => ensureGitignore,
|
|
4958
5029
|
deleteProjectState: () => deleteProjectState,
|
|
4959
|
-
createInitialState: () => createInitialState
|
|
5030
|
+
createInitialState: () => createInitialState,
|
|
5031
|
+
STATE_VERSION: () => STATE_VERSION
|
|
4960
5032
|
});
|
|
4961
5033
|
function getStatePath(directory) {
|
|
4962
5034
|
return `${directory}/${STATE_DIR}/${STATE_FILE}`;
|
|
@@ -4976,9 +5048,44 @@ async function loadProjectState(directory) {
|
|
|
4976
5048
|
}
|
|
4977
5049
|
try {
|
|
4978
5050
|
const content = await Bun.file(statePath).text();
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
5051
|
+
if (!content.trim()) {
|
|
5052
|
+
logger5.warn("Empty state file detected, removing");
|
|
5053
|
+
await deleteProjectState(directory);
|
|
5054
|
+
return null;
|
|
5055
|
+
}
|
|
5056
|
+
let data;
|
|
5057
|
+
try {
|
|
5058
|
+
data = JSON.parse(content);
|
|
5059
|
+
} catch (parseError) {
|
|
5060
|
+
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
|
5061
|
+
logger5.error("Corrupt JSON in state file, attempting backup recovery", { error: errorMessage });
|
|
5062
|
+
const recovered2 = await recoverFromBackup(directory);
|
|
5063
|
+
if (recovered2) {
|
|
5064
|
+
return recovered2;
|
|
5065
|
+
}
|
|
5066
|
+
await deleteProjectState(directory);
|
|
5067
|
+
return null;
|
|
5068
|
+
}
|
|
5069
|
+
const validationResult = ProjectStateSchema.safeParse(data);
|
|
5070
|
+
if (validationResult.success) {
|
|
5071
|
+
const state = await migrateState(data, directory);
|
|
5072
|
+
return state;
|
|
5073
|
+
}
|
|
5074
|
+
logger5.warn("State validation failed, attempting repair", {
|
|
5075
|
+
errors: validationResult.error.format()
|
|
5076
|
+
});
|
|
5077
|
+
const repairedState = await repairState(data, directory);
|
|
5078
|
+
if (repairedState) {
|
|
5079
|
+
logger5.info("State successfully repaired");
|
|
5080
|
+
return repairedState;
|
|
5081
|
+
}
|
|
5082
|
+
logger5.error("State repair failed, attempting backup recovery");
|
|
5083
|
+
const recovered = await recoverFromBackup(directory);
|
|
5084
|
+
if (recovered) {
|
|
5085
|
+
return recovered;
|
|
5086
|
+
}
|
|
5087
|
+
logger5.error("Failed to load project state - no recovery possible");
|
|
5088
|
+
return null;
|
|
4982
5089
|
} catch (error45) {
|
|
4983
5090
|
logger5.error("Failed to load project state", {
|
|
4984
5091
|
error: error45 instanceof Error ? error45.message : String(error45),
|
|
@@ -4987,14 +5094,132 @@ async function loadProjectState(directory) {
|
|
|
4987
5094
|
return null;
|
|
4988
5095
|
}
|
|
4989
5096
|
}
|
|
5097
|
+
async function migrateState(data, directory) {
|
|
5098
|
+
const currentVersion = data._version ?? 1;
|
|
5099
|
+
if (currentVersion >= STATE_VERSION) {
|
|
5100
|
+
const { _version: _version2, ...state } = data;
|
|
5101
|
+
return state;
|
|
5102
|
+
}
|
|
5103
|
+
logger5.info("Migrating state", { from: currentVersion, to: STATE_VERSION });
|
|
5104
|
+
let migratedData = { ...data };
|
|
5105
|
+
if (currentVersion < 2) {
|
|
5106
|
+
migratedData = {
|
|
5107
|
+
...migratedData,
|
|
5108
|
+
languages: migratedData.languages ?? [],
|
|
5109
|
+
setupComplete: migratedData.setupComplete ?? true
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
5112
|
+
migratedData._version = STATE_VERSION;
|
|
5113
|
+
const { _version, ...cleanState } = migratedData;
|
|
5114
|
+
const finalState = cleanState;
|
|
5115
|
+
await createBackup(directory);
|
|
5116
|
+
await saveProjectState(directory, finalState);
|
|
5117
|
+
logger5.info("State migration complete");
|
|
5118
|
+
return finalState;
|
|
5119
|
+
}
|
|
5120
|
+
function extractString(data, key, defaultValue) {
|
|
5121
|
+
const value = data[key];
|
|
5122
|
+
return typeof value === "string" && value.length > 0 ? value : defaultValue;
|
|
5123
|
+
}
|
|
5124
|
+
function extractBoolean(data, key, defaultValue) {
|
|
5125
|
+
const value = data[key];
|
|
5126
|
+
return typeof value === "boolean" ? value : defaultValue;
|
|
5127
|
+
}
|
|
5128
|
+
function extractStringArray(data, key) {
|
|
5129
|
+
const value = data[key];
|
|
5130
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
5131
|
+
}
|
|
5132
|
+
function extractProjectKey(data, directory) {
|
|
5133
|
+
const key = extractString(data, "projectKey");
|
|
5134
|
+
if (key)
|
|
5135
|
+
return key;
|
|
5136
|
+
const dirName = directory.split("/").pop();
|
|
5137
|
+
return dirName && dirName.length > 0 ? dirName : null;
|
|
5138
|
+
}
|
|
5139
|
+
async function repairState(data, directory) {
|
|
5140
|
+
await createBackup(directory);
|
|
5141
|
+
const projectKey = extractProjectKey(data, directory);
|
|
5142
|
+
const projectToken = extractString(data, "projectToken");
|
|
5143
|
+
if (!projectKey || !projectToken) {
|
|
5144
|
+
return null;
|
|
5145
|
+
}
|
|
5146
|
+
const repaired = {
|
|
5147
|
+
projectKey,
|
|
5148
|
+
projectToken,
|
|
5149
|
+
tokenName: extractString(data, "tokenName") ?? `opencode-${projectKey}-recovered`,
|
|
5150
|
+
initializedAt: extractString(data, "initializedAt") ?? new Date().toISOString(),
|
|
5151
|
+
languages: extractStringArray(data, "languages"),
|
|
5152
|
+
qualityGate: extractString(data, "qualityGate"),
|
|
5153
|
+
lastAnalysis: extractString(data, "lastAnalysis"),
|
|
5154
|
+
setupComplete: extractBoolean(data, "setupComplete", true)
|
|
5155
|
+
};
|
|
5156
|
+
const validationResult = ProjectStateSchema.safeParse(repaired);
|
|
5157
|
+
if (!validationResult.success) {
|
|
5158
|
+
logger5.error("Repaired state still invalid", { errors: validationResult.error.format() });
|
|
5159
|
+
return null;
|
|
5160
|
+
}
|
|
5161
|
+
await saveProjectState(directory, validationResult.data);
|
|
5162
|
+
return validationResult.data;
|
|
5163
|
+
}
|
|
5164
|
+
async function createBackup(directory) {
|
|
5165
|
+
const statePath = getStatePath(directory);
|
|
5166
|
+
const backupPath = `${directory}/${STATE_DIR}/${BACKUP_FILE}`;
|
|
5167
|
+
try {
|
|
5168
|
+
const exists = await Bun.file(statePath).exists();
|
|
5169
|
+
if (!exists) {
|
|
5170
|
+
return false;
|
|
5171
|
+
}
|
|
5172
|
+
const content = await Bun.file(statePath).text();
|
|
5173
|
+
await Bun.write(backupPath, content);
|
|
5174
|
+
logger5.info("Created state backup");
|
|
5175
|
+
return true;
|
|
5176
|
+
} catch {
|
|
5177
|
+
logger5.warn("Failed to create state backup");
|
|
5178
|
+
return false;
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
async function recoverFromBackup(directory) {
|
|
5182
|
+
const backupPath = `${directory}/${STATE_DIR}/${BACKUP_FILE}`;
|
|
5183
|
+
try {
|
|
5184
|
+
const exists = await Bun.file(backupPath).exists();
|
|
5185
|
+
if (!exists) {
|
|
5186
|
+
return null;
|
|
5187
|
+
}
|
|
5188
|
+
const content = await Bun.file(backupPath).text();
|
|
5189
|
+
if (!content.trim()) {
|
|
5190
|
+
return null;
|
|
5191
|
+
}
|
|
5192
|
+
const data = JSON.parse(content);
|
|
5193
|
+
const validationResult = ProjectStateSchema.safeParse(data);
|
|
5194
|
+
if (!validationResult.success) {
|
|
5195
|
+
return null;
|
|
5196
|
+
}
|
|
5197
|
+
await saveProjectState(directory, validationResult.data);
|
|
5198
|
+
logger5.info("State recovered from backup");
|
|
5199
|
+
return validationResult.data;
|
|
5200
|
+
} catch {
|
|
5201
|
+
return null;
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
4990
5204
|
async function saveProjectState(directory, state) {
|
|
4991
5205
|
const stateDir = getStateDir(directory);
|
|
4992
5206
|
const statePath = getStatePath(directory);
|
|
4993
5207
|
const { mkdir } = await import("node:fs/promises");
|
|
4994
5208
|
await mkdir(stateDir, { recursive: true });
|
|
4995
|
-
const
|
|
5209
|
+
const existingFile = Bun.file(statePath);
|
|
5210
|
+
if (await existingFile.exists()) {
|
|
5211
|
+
try {
|
|
5212
|
+
const existingContent = await existingFile.text();
|
|
5213
|
+
if (existingContent.trim()) {
|
|
5214
|
+
const backupPath = `${stateDir}/${BACKUP_FILE}`;
|
|
5215
|
+
await Bun.write(backupPath, existingContent);
|
|
5216
|
+
}
|
|
5217
|
+
} catch {}
|
|
5218
|
+
}
|
|
5219
|
+
const versionedState = { ...state, _version: STATE_VERSION };
|
|
5220
|
+
const content = JSON.stringify(versionedState, null, 2);
|
|
4996
5221
|
await Bun.write(statePath, content);
|
|
4997
|
-
logger5.info("Saved project state", { projectKey: state.projectKey });
|
|
5222
|
+
logger5.info("Saved project state", { projectKey: state.projectKey, version: STATE_VERSION });
|
|
4998
5223
|
}
|
|
4999
5224
|
async function updateProjectState(directory, updates) {
|
|
5000
5225
|
const existing = await loadProjectState(directory);
|
|
@@ -5063,7 +5288,7 @@ ${entry}
|
|
|
5063
5288
|
await Bun.write(gitignorePath, newContent);
|
|
5064
5289
|
logger5.info("Added SonarQube exclusion to .gitignore");
|
|
5065
5290
|
}
|
|
5066
|
-
var logger5, STATE_DIR = ".sonarqube", STATE_FILE = "project.json";
|
|
5291
|
+
var logger5, STATE_DIR = ".sonarqube", STATE_FILE = "project.json", BACKUP_FILE = "project.json.backup", STATE_VERSION = 2;
|
|
5067
5292
|
var init_state = __esm(() => {
|
|
5068
5293
|
init_types2();
|
|
5069
5294
|
init_logger();
|
|
@@ -19952,7 +20177,51 @@ Please check:
|
|
|
19952
20177
|
return msg;
|
|
19953
20178
|
},
|
|
19954
20179
|
connectionError(serverUrl) {
|
|
19955
|
-
return `Cannot connect to SonarQube server at ${serverUrl}
|
|
20180
|
+
return `Cannot connect to SonarQube server at ${serverUrl}
|
|
20181
|
+
|
|
20182
|
+
**The server appears to be offline or unreachable.**
|
|
20183
|
+
|
|
20184
|
+
Possible causes:
|
|
20185
|
+
1. Server is down or restarting
|
|
20186
|
+
2. Network connectivity issues (VPN, firewall)
|
|
20187
|
+
3. Incorrect URL in configuration
|
|
20188
|
+
|
|
20189
|
+
What you can do:
|
|
20190
|
+
- Continue coding - the plugin will auto-retry when the server is available
|
|
20191
|
+
- Check server status manually: curl -s "${serverUrl}/api/system/status"
|
|
20192
|
+
- Verify your network connection
|
|
20193
|
+
- Try again later with: sonarqube({ action: "status" })
|
|
20194
|
+
|
|
20195
|
+
Your last analysis results are preserved in .sonarqube/project.json`;
|
|
20196
|
+
},
|
|
20197
|
+
timeoutError(action, timeoutMs) {
|
|
20198
|
+
return `Action \`${action}\` timed out after ${timeoutMs / 1000} seconds.
|
|
20199
|
+
|
|
20200
|
+
This usually means:
|
|
20201
|
+
1. The SonarQube server is under heavy load
|
|
20202
|
+
2. Network latency is very high
|
|
20203
|
+
3. The analysis is taking longer than expected
|
|
20204
|
+
|
|
20205
|
+
Suggestions:
|
|
20206
|
+
- Try again in a few minutes
|
|
20207
|
+
- For large projects, analysis may take several minutes
|
|
20208
|
+
- Check server health: sonarqube({ action: "status" })`;
|
|
20209
|
+
},
|
|
20210
|
+
offlineMode(cachedData) {
|
|
20211
|
+
let msg = `**Operating in offline mode** - SonarQube server is not reachable.`;
|
|
20212
|
+
if (cachedData?.lastAnalysis) {
|
|
20213
|
+
msg += `
|
|
20214
|
+
|
|
20215
|
+
Last known status (from ${cachedData.lastAnalysis}):`;
|
|
20216
|
+
if (cachedData.qualityGate) {
|
|
20217
|
+
msg += `
|
|
20218
|
+
- Quality Gate: ${cachedData.qualityGate}`;
|
|
20219
|
+
}
|
|
20220
|
+
}
|
|
20221
|
+
msg += `
|
|
20222
|
+
|
|
20223
|
+
The plugin will automatically reconnect when the server is available.`;
|
|
20224
|
+
return msg;
|
|
19956
20225
|
},
|
|
19957
20226
|
authenticationError(reason) {
|
|
19958
20227
|
return `Authentication failed: ${reason}`;
|
|
@@ -20713,80 +20982,94 @@ var SonarQubeToolArgsSchema = exports_external2.object({
|
|
|
20713
20982
|
resolution: exports_external2.enum(["SAFE", "FIXED", "ACKNOWLEDGED"]).optional().describe("Hotspot review resolution: SAFE (no risk), FIXED (remediated), ACKNOWLEDGED (accepted risk)"),
|
|
20714
20983
|
comment: exports_external2.string().optional().describe("Review comment explaining the decision")
|
|
20715
20984
|
});
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
"Set environment variables: SONAR_HOST_URL, SONAR_USER, and SONAR_PASSWORD",
|
|
20723
|
-
"Or create a .sonarqube/config.json file in your project",
|
|
20724
|
-
"Or add plugin configuration in opencode.json"
|
|
20725
|
-
]) + `
|
|
20985
|
+
function getMissingConfigError() {
|
|
20986
|
+
return formatError2(ErrorMessages.configurationMissing("SonarQube configuration not found.", [
|
|
20987
|
+
"Set environment variables: SONAR_HOST_URL, SONAR_USER, and SONAR_PASSWORD",
|
|
20988
|
+
"Or create a .sonarqube/config.json file in your project",
|
|
20989
|
+
"Or add plugin configuration in opencode.json"
|
|
20990
|
+
]) + `
|
|
20726
20991
|
|
|
20727
20992
|
Required environment variables:
|
|
20728
20993
|
- SONAR_HOST_URL (e.g., https://sonarqube.company.com)
|
|
20729
20994
|
- SONAR_USER (e.g., admin)
|
|
20730
20995
|
- SONAR_PASSWORD (password or token)`);
|
|
20996
|
+
}
|
|
20997
|
+
async function ensureBootstrapped(config3, directory) {
|
|
20998
|
+
if (!await needsBootstrap(directory)) {
|
|
20999
|
+
return null;
|
|
21000
|
+
}
|
|
21001
|
+
logger10.info("First run detected, running bootstrap");
|
|
21002
|
+
const { bootstrap: bootstrap2 } = await Promise.resolve().then(() => (init_bootstrap(), exports_bootstrap));
|
|
21003
|
+
const result = await bootstrap2({ config: config3, directory });
|
|
21004
|
+
if (!result.success) {
|
|
21005
|
+
return formatError2(`Setup Failed
|
|
21006
|
+
|
|
21007
|
+
${result.message}`);
|
|
21008
|
+
}
|
|
21009
|
+
logger10.info("Bootstrap completed", { projectKey: result.projectKey });
|
|
21010
|
+
return null;
|
|
21011
|
+
}
|
|
21012
|
+
async function routeAction(ctx, args) {
|
|
21013
|
+
const handlers = {
|
|
21014
|
+
analyze: () => handleAnalyze(ctx, args),
|
|
21015
|
+
issues: () => handleIssues(ctx, args),
|
|
21016
|
+
newissues: () => handleNewIssues(ctx, args),
|
|
21017
|
+
status: () => handleStatus(ctx),
|
|
21018
|
+
validate: () => handleValidate(ctx),
|
|
21019
|
+
hotspots: () => handleHotspots(ctx),
|
|
21020
|
+
reviewhotspot: () => handleReviewHotspot(ctx, args.hotspotKey, args.resolution, args.comment),
|
|
21021
|
+
duplications: () => handleDuplications(ctx),
|
|
21022
|
+
rule: () => handleRule(ctx, args.ruleKey),
|
|
21023
|
+
history: () => handleHistory(ctx, args.branch),
|
|
21024
|
+
profile: () => handleProfile(ctx),
|
|
21025
|
+
branches: () => handleBranches(ctx),
|
|
21026
|
+
metrics: () => handleMetrics(ctx, args.branch),
|
|
21027
|
+
worstfiles: () => handleWorstFiles(ctx)
|
|
21028
|
+
};
|
|
21029
|
+
const handler = handlers[args.action];
|
|
21030
|
+
return handler ? handler() : formatError2(`Unknown action: ${args.action}`);
|
|
21031
|
+
}
|
|
21032
|
+
function handleExecutionError(error45, action, serverUrl) {
|
|
21033
|
+
const errorMessage = error45 instanceof Error ? error45.message : String(error45);
|
|
21034
|
+
logger10.error(`Tool execution failed: ${errorMessage}`);
|
|
21035
|
+
if (!(error45 instanceof Error) || !("code" in error45)) {
|
|
21036
|
+
return formatError2(ErrorMessages.apiError(action, errorMessage, serverUrl));
|
|
21037
|
+
}
|
|
21038
|
+
const errorCode = error45.code;
|
|
21039
|
+
if (errorCode === "CONNECTION_ERROR" || errorMessage.includes("Cannot connect")) {
|
|
21040
|
+
return formatError2(ErrorMessages.connectionError(serverUrl));
|
|
21041
|
+
}
|
|
21042
|
+
if (errorMessage.includes("timed out")) {
|
|
21043
|
+
return formatError2(ErrorMessages.timeoutError(action, 30000));
|
|
21044
|
+
}
|
|
21045
|
+
if (errorCode === "AUTH_ERROR") {
|
|
21046
|
+
return formatError2(ErrorMessages.authenticationError(errorMessage));
|
|
21047
|
+
}
|
|
21048
|
+
return formatError2(ErrorMessages.apiError(action, errorMessage, serverUrl));
|
|
21049
|
+
}
|
|
21050
|
+
async function executeSonarQubeTool(args, context) {
|
|
21051
|
+
const directory = context.directory ?? process.cwd();
|
|
21052
|
+
const rawConfig = context.config?.["sonarqube"] ?? context.config;
|
|
21053
|
+
const config3 = loadConfig(rawConfig);
|
|
21054
|
+
if (!config3) {
|
|
21055
|
+
return getMissingConfigError();
|
|
20731
21056
|
}
|
|
20732
21057
|
logger10.info(`Executing SonarQube tool: ${args.action}`, { directory });
|
|
20733
21058
|
try {
|
|
20734
21059
|
if (args.action === "init" || args.action === "setup") {
|
|
20735
21060
|
return await handleSetup(config3, directory, args.force);
|
|
20736
21061
|
}
|
|
20737
|
-
|
|
20738
|
-
|
|
20739
|
-
|
|
20740
|
-
const setupResult = await bootstrap2({ config: config3, directory });
|
|
20741
|
-
if (!setupResult.success) {
|
|
20742
|
-
return formatError2(`Setup Failed
|
|
20743
|
-
|
|
20744
|
-
${setupResult.message}`);
|
|
20745
|
-
}
|
|
20746
|
-
logger10.info("Bootstrap completed", { projectKey: setupResult.projectKey });
|
|
20747
|
-
}
|
|
21062
|
+
const bootstrapError = await ensureBootstrapped(config3, directory);
|
|
21063
|
+
if (bootstrapError)
|
|
21064
|
+
return bootstrapError;
|
|
20748
21065
|
const state = await getProjectState(directory);
|
|
20749
21066
|
if (!state) {
|
|
20750
21067
|
return formatError2(`Project not initialized. Run with action: "setup" first.`);
|
|
20751
21068
|
}
|
|
20752
|
-
const
|
|
20753
|
-
|
|
20754
|
-
switch (args.action) {
|
|
20755
|
-
case "analyze":
|
|
20756
|
-
return await handleAnalyze(ctx, args);
|
|
20757
|
-
case "issues":
|
|
20758
|
-
return await handleIssues(ctx, args);
|
|
20759
|
-
case "newissues":
|
|
20760
|
-
return await handleNewIssues(ctx, args);
|
|
20761
|
-
case "status":
|
|
20762
|
-
return await handleStatus(ctx);
|
|
20763
|
-
case "validate":
|
|
20764
|
-
return await handleValidate(ctx);
|
|
20765
|
-
case "hotspots":
|
|
20766
|
-
return await handleHotspots(ctx);
|
|
20767
|
-
case "reviewhotspot":
|
|
20768
|
-
return await handleReviewHotspot(ctx, args.hotspotKey, args.resolution, args.comment);
|
|
20769
|
-
case "duplications":
|
|
20770
|
-
return await handleDuplications(ctx);
|
|
20771
|
-
case "rule":
|
|
20772
|
-
return await handleRule(ctx, args.ruleKey);
|
|
20773
|
-
case "history":
|
|
20774
|
-
return await handleHistory(ctx, args.branch);
|
|
20775
|
-
case "profile":
|
|
20776
|
-
return await handleProfile(ctx);
|
|
20777
|
-
case "branches":
|
|
20778
|
-
return await handleBranches(ctx);
|
|
20779
|
-
case "metrics":
|
|
20780
|
-
return await handleMetrics(ctx, args.branch);
|
|
20781
|
-
case "worstfiles":
|
|
20782
|
-
return await handleWorstFiles(ctx);
|
|
20783
|
-
default:
|
|
20784
|
-
return formatError2(`Unknown action: ${args.action}`);
|
|
20785
|
-
}
|
|
21069
|
+
const ctx = createHandlerContext(config3, state, args.projectKey ?? state.projectKey, directory);
|
|
21070
|
+
return await routeAction(ctx, args);
|
|
20786
21071
|
} catch (error45) {
|
|
20787
|
-
|
|
20788
|
-
logger10.error(`Tool execution failed: ${errorMessage}`);
|
|
20789
|
-
return formatError2(ErrorMessages.apiError(args.action, errorMessage, config3.url));
|
|
21072
|
+
return handleExecutionError(error45, args.action, config3.url);
|
|
20790
21073
|
}
|
|
20791
21074
|
}
|
|
20792
21075
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sonarqube",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "OpenCode Plugin for SonarQube integration - Enterprise-level code quality from the start",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"homepage": "https://github.com/mguttmann/opencode-sonarqube#readme",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@opencode-ai/plugin": "^1.1.34",
|
|
41
|
-
"opencode-sonarqube": "latest",
|
|
42
41
|
"zod": "^3.24.0"
|
|
43
42
|
},
|
|
44
43
|
"devDependencies": {
|