automata-cli 0.3.0-develop.96 → 0.3.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/dist/index.js +477 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,10 +19,12 @@ import React2 from "react";
|
|
|
19
19
|
// src/config/ConfigWizard.tsx
|
|
20
20
|
import { useState } from "react";
|
|
21
21
|
import { Box, Text, useInput, useApp } from "ink";
|
|
22
|
+
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
23
|
+
import { join as join2 } from "path";
|
|
22
24
|
|
|
23
25
|
// src/config/configStore.ts
|
|
24
26
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
25
|
-
import { join } from "path";
|
|
27
|
+
import { join, resolve as resolve2, sep } from "path";
|
|
26
28
|
var DEFAULT_CLAUDE_SYSTEM_PROMPT = "You are an expert software engineer. Implement the following issue according to the project's existing conventions and style. Make minimal, targeted changes that satisfy the requirements. Run tests and linting before finishing.";
|
|
27
29
|
var DEFAULT_FIX_COMMENTS_PROMPT = "You are an expert software engineer reviewing a pull request. Below are the open review comments left by reviewers on this PR. Please address each comment by making the appropriate code changes. Focus on the reviewer's concerns and make minimal, targeted changes that resolve each comment without altering unrelated code.";
|
|
28
30
|
var DEFAULT_SONAR_PROMPT = "You are an expert software engineer. You have been given the URL of a SonarCloud analysis for this pull request. If the `sonar-quality-gate` skill is available in this repository, use it. The project is public, so use the SonarCloud REST API directly (no authentication required) rather than scraping the URL. Inspect both the quality gate and the list of issues for this pull request. If the quality gate fails because of duplication or another metric-based condition, use the relevant Sonar APIs to identify the affected files and details instead of relying only on the issues endpoint. Fix all new issues and quality-gate failures reported. Focus on code smells, bugs, vulnerabilities, and blocking quality-gate conditions flagged in this PR. Make targeted, minimal changes that resolve each issue without altering unrelated code.";
|
|
@@ -31,7 +33,32 @@ var CONFIG_FILE = "config.json";
|
|
|
31
33
|
function configPath() {
|
|
32
34
|
return join(process.cwd(), CONFIG_DIR, CONFIG_FILE);
|
|
33
35
|
}
|
|
34
|
-
function
|
|
36
|
+
function automataDir() {
|
|
37
|
+
return join(process.cwd(), CONFIG_DIR);
|
|
38
|
+
}
|
|
39
|
+
function resolvePromptRef(value, dir) {
|
|
40
|
+
if (!value.endsWith(".md")) return value;
|
|
41
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
42
|
+
throw new Error(`Prompt file "${value}" must be a plain filename with no subdirectories`);
|
|
43
|
+
}
|
|
44
|
+
const fullPath = resolve2(dir, value);
|
|
45
|
+
const safeBase = resolve2(dir) + sep;
|
|
46
|
+
if (!fullPath.startsWith(safeBase)) {
|
|
47
|
+
throw new Error(`Prompt file "${value}" resolves outside .automata/`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return readFileSync2(fullPath, "utf8");
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err?.code === "ENOENT") {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Prompt file "${value}" not found in "${dir}". Expected path: ${fullPath}`,
|
|
55
|
+
{ cause: err }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function readRawConfig() {
|
|
35
62
|
try {
|
|
36
63
|
const raw = readFileSync2(configPath(), "utf8");
|
|
37
64
|
return JSON.parse(raw);
|
|
@@ -39,6 +66,26 @@ function readConfig() {
|
|
|
39
66
|
return {};
|
|
40
67
|
}
|
|
41
68
|
}
|
|
69
|
+
function readConfig() {
|
|
70
|
+
let config;
|
|
71
|
+
try {
|
|
72
|
+
const raw = readFileSync2(configPath(), "utf8");
|
|
73
|
+
config = JSON.parse(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
const dir = automataDir();
|
|
78
|
+
if (config.claudeSystemPrompt) {
|
|
79
|
+
config.claudeSystemPrompt = resolvePromptRef(config.claudeSystemPrompt, dir);
|
|
80
|
+
}
|
|
81
|
+
if (config.prompts?.sonar) {
|
|
82
|
+
config.prompts.sonar = resolvePromptRef(config.prompts.sonar, dir);
|
|
83
|
+
}
|
|
84
|
+
if (config.prompts?.fixComments) {
|
|
85
|
+
config.prompts.fixComments = resolvePromptRef(config.prompts.fixComments, dir);
|
|
86
|
+
}
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
42
89
|
function writeConfig(config) {
|
|
43
90
|
const dir = join(process.cwd(), CONFIG_DIR);
|
|
44
91
|
mkdirSync(dir, { recursive: true });
|
|
@@ -47,6 +94,11 @@ function writeConfig(config) {
|
|
|
47
94
|
|
|
48
95
|
// src/config/ConfigWizard.tsx
|
|
49
96
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
97
|
+
function writePromptFile(filename, content) {
|
|
98
|
+
const dir = join2(process.cwd(), ".automata");
|
|
99
|
+
mkdirSync2(dir, { recursive: true });
|
|
100
|
+
writeFileSync2(join2(dir, filename), content, "utf8");
|
|
101
|
+
}
|
|
50
102
|
var REMOTE_OPTIONS = [
|
|
51
103
|
{ label: "GitHub", value: "gh" },
|
|
52
104
|
{ label: "Azure DevOps", value: "azdo" }
|
|
@@ -60,6 +112,7 @@ var MAIN_MENU_OPTIONS = ["Remote / Mode", "Implement-Next", "Prompts"];
|
|
|
60
112
|
var PROMPTS_MENU_OPTIONS = ["Sonar", "Fix-Comments"];
|
|
61
113
|
function ConfigWizard() {
|
|
62
114
|
const existing = readConfig();
|
|
115
|
+
const rawExisting = readRawConfig();
|
|
63
116
|
const initialRemoteIndex = REMOTE_OPTIONS.findIndex((o) => o.value === existing.remoteType);
|
|
64
117
|
const initialTechIndex = TECHNIQUE_OPTIONS.findIndex((o) => o.value === existing.issueDiscoveryTechnique);
|
|
65
118
|
const [screen, setScreen] = useState("main");
|
|
@@ -107,7 +160,7 @@ function ConfigWizard() {
|
|
|
107
160
|
if (chosen.value === "gh") {
|
|
108
161
|
setScreen("technique");
|
|
109
162
|
} else {
|
|
110
|
-
writeConfig({ ...
|
|
163
|
+
writeConfig({ ...rawExisting, remoteType: chosen.value });
|
|
111
164
|
exit();
|
|
112
165
|
}
|
|
113
166
|
} else if (key.escape) {
|
|
@@ -143,12 +196,17 @@ function ConfigWizard() {
|
|
|
143
196
|
}
|
|
144
197
|
} else if (screen === "system-prompt") {
|
|
145
198
|
if (key.return) {
|
|
199
|
+
let claudeSystemPromptValue;
|
|
200
|
+
if (systemPrompt) {
|
|
201
|
+
writePromptFile("claude-system-prompt.md", systemPrompt);
|
|
202
|
+
claudeSystemPromptValue = "claude-system-prompt.md";
|
|
203
|
+
}
|
|
146
204
|
writeConfig({
|
|
147
|
-
...
|
|
205
|
+
...rawExisting,
|
|
148
206
|
remoteType: pendingRemote,
|
|
149
207
|
issueDiscoveryTechnique: pendingTechnique,
|
|
150
208
|
issueDiscoveryValue: discoveryValue || void 0,
|
|
151
|
-
claudeSystemPrompt:
|
|
209
|
+
claudeSystemPrompt: claudeSystemPromptValue
|
|
152
210
|
});
|
|
153
211
|
exit();
|
|
154
212
|
} else if (key.backspace || key.delete) {
|
|
@@ -179,10 +237,15 @@ function ConfigWizard() {
|
|
|
179
237
|
}
|
|
180
238
|
} else if (screen === "sonar-prompt") {
|
|
181
239
|
if (key.return) {
|
|
182
|
-
|
|
240
|
+
let sonarValue;
|
|
241
|
+
if (sonarPrompt) {
|
|
242
|
+
writePromptFile("sonar-prompt.md", sonarPrompt);
|
|
243
|
+
sonarValue = "sonar-prompt.md";
|
|
244
|
+
}
|
|
245
|
+
const current = readRawConfig();
|
|
183
246
|
writeConfig({
|
|
184
247
|
...current,
|
|
185
|
-
prompts: { ...current.prompts, sonar:
|
|
248
|
+
prompts: { ...current.prompts, sonar: sonarValue }
|
|
186
249
|
});
|
|
187
250
|
setScreen("prompts-menu");
|
|
188
251
|
} else if (key.backspace || key.delete) {
|
|
@@ -196,10 +259,15 @@ function ConfigWizard() {
|
|
|
196
259
|
}
|
|
197
260
|
} else if (screen === "fix-comments-prompt") {
|
|
198
261
|
if (key.return) {
|
|
199
|
-
|
|
262
|
+
let fixCommentsValue;
|
|
263
|
+
if (fixCommentsPrompt) {
|
|
264
|
+
writePromptFile("fix-comments-prompt.md", fixCommentsPrompt);
|
|
265
|
+
fixCommentsValue = "fix-comments-prompt.md";
|
|
266
|
+
}
|
|
267
|
+
const current = readRawConfig();
|
|
200
268
|
writeConfig({
|
|
201
269
|
...current,
|
|
202
|
-
prompts: { ...current.prompts, fixComments:
|
|
270
|
+
prompts: { ...current.prompts, fixComments: fixCommentsValue }
|
|
203
271
|
});
|
|
204
272
|
setScreen("prompts-menu");
|
|
205
273
|
} else if (key.backspace || key.delete) {
|
|
@@ -338,7 +406,7 @@ var configSetType = new Command("type").description("Set the remote environment
|
|
|
338
406
|
`);
|
|
339
407
|
process.exit(1);
|
|
340
408
|
}
|
|
341
|
-
const current =
|
|
409
|
+
const current = readRawConfig();
|
|
342
410
|
writeConfig({ ...current, remoteType: value });
|
|
343
411
|
process.stdout.write(`Remote type set to: ${value}
|
|
344
412
|
`);
|
|
@@ -349,19 +417,19 @@ var configSetIssueDiscoveryTechnique = new Command("issue-discovery-technique").
|
|
|
349
417
|
`);
|
|
350
418
|
process.exit(1);
|
|
351
419
|
}
|
|
352
|
-
const current =
|
|
420
|
+
const current = readRawConfig();
|
|
353
421
|
writeConfig({ ...current, issueDiscoveryTechnique: value });
|
|
354
422
|
process.stdout.write(`Issue discovery technique set to: ${value}
|
|
355
423
|
`);
|
|
356
424
|
});
|
|
357
425
|
var configSetIssueDiscoveryValue = new Command("issue-discovery-value").description("Set the value for the issue discovery technique (label name, username, or search string)").argument("<value>", "The filter value").action((value) => {
|
|
358
|
-
const current =
|
|
426
|
+
const current = readRawConfig();
|
|
359
427
|
writeConfig({ ...current, issueDiscoveryValue: value });
|
|
360
428
|
process.stdout.write(`Issue discovery value set to: ${value}
|
|
361
429
|
`);
|
|
362
430
|
});
|
|
363
431
|
var configSetClaudeSystemPrompt = new Command("claude-system-prompt").description("Set the system prompt used when invoking Claude Code").argument("<value>", "System prompt text").action((value) => {
|
|
364
|
-
const current =
|
|
432
|
+
const current = readRawConfig();
|
|
365
433
|
writeConfig({ ...current, claudeSystemPrompt: value });
|
|
366
434
|
process.stdout.write(`Claude system prompt set.
|
|
367
435
|
`);
|
|
@@ -442,6 +510,8 @@ function getCurrentBranch() {
|
|
|
442
510
|
}
|
|
443
511
|
return stdout.trim();
|
|
444
512
|
}
|
|
513
|
+
var SONAR_FETCH_TIMEOUT_MS = 5e3;
|
|
514
|
+
var SONAR_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
|
|
445
515
|
function parseOwnerRepo() {
|
|
446
516
|
const { stdout, status } = run2("git", ["remote", "get-url", "origin"]);
|
|
447
517
|
if (status !== 0) return null;
|
|
@@ -487,21 +557,285 @@ function extractSonarProjectKey(url) {
|
|
|
487
557
|
return null;
|
|
488
558
|
}
|
|
489
559
|
}
|
|
490
|
-
|
|
491
|
-
|
|
560
|
+
function isSonarUrl(url) {
|
|
561
|
+
try {
|
|
562
|
+
const hostname = new URL(url).hostname;
|
|
563
|
+
return hostname === "sonarcloud.io" || hostname.endsWith(".sonarcloud.io");
|
|
564
|
+
} catch {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function stripHtml(text) {
|
|
569
|
+
let stripped = "";
|
|
570
|
+
let inTag = false;
|
|
571
|
+
for (const char of text) {
|
|
572
|
+
if (char === "<") {
|
|
573
|
+
inTag = true;
|
|
574
|
+
stripped += " ";
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (char === ">") {
|
|
578
|
+
inTag = false;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (!inTag) {
|
|
582
|
+
stripped += char;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return stripped;
|
|
586
|
+
}
|
|
587
|
+
function normalizeText(text) {
|
|
588
|
+
if (!text) return void 0;
|
|
589
|
+
const normalized = stripHtml(text).replaceAll(/\s+/g, " ").trim();
|
|
590
|
+
return normalized || void 0;
|
|
591
|
+
}
|
|
592
|
+
function resolveSonarPath(componentKey, components) {
|
|
593
|
+
if (!componentKey) return void 0;
|
|
594
|
+
const mapped = components.get(componentKey);
|
|
595
|
+
if (mapped) return mapped;
|
|
596
|
+
const separatorIndex = componentKey.indexOf(":");
|
|
597
|
+
if (separatorIndex === -1) return void 0;
|
|
598
|
+
const path = componentKey.slice(separatorIndex + 1).trim();
|
|
599
|
+
return path || void 0;
|
|
600
|
+
}
|
|
601
|
+
function mapSonarIssuesPage(data, targetIssues, components, rules) {
|
|
602
|
+
for (const component of data.components ?? []) {
|
|
603
|
+
if (component.key && component.path) {
|
|
604
|
+
components.set(component.key, component.path);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for (const rule of data.rules ?? []) {
|
|
608
|
+
if (!rule.key) continue;
|
|
609
|
+
const explanation = normalizeText(rule.htmlDesc) ?? normalizeText(rule.htmlNote) ?? normalizeText(rule.name);
|
|
610
|
+
if (explanation) {
|
|
611
|
+
rules.set(rule.key, explanation);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
for (const issue of data.issues ?? []) {
|
|
615
|
+
if (!issue.key || !issue.message) continue;
|
|
616
|
+
const ruleKey = issue.rule;
|
|
617
|
+
const mappedIssue = {
|
|
618
|
+
key: issue.key,
|
|
619
|
+
severity: issue.severity,
|
|
620
|
+
type: issue.type,
|
|
621
|
+
message: issue.message,
|
|
622
|
+
path: resolveSonarPath(issue.component, components),
|
|
623
|
+
line: issue.line ?? issue.textRange?.startLine ?? null,
|
|
624
|
+
...ruleKey ? { rule: ruleKey } : {},
|
|
625
|
+
...ruleKey && rules.has(ruleKey) ? { explanation: rules.get(ruleKey) } : {}
|
|
626
|
+
};
|
|
627
|
+
targetIssues.push(mappedIssue);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function mapSonarHotspotsPage(data, targetHotspots, components) {
|
|
631
|
+
for (const component of data.components ?? []) {
|
|
632
|
+
if (component.key && component.path) {
|
|
633
|
+
components.set(component.key, component.path);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
targetHotspots.push(...data.hotspots ?? []);
|
|
637
|
+
}
|
|
638
|
+
function mapSonarHotspot(hotspot, components, detail) {
|
|
639
|
+
const rule = detail?.rule;
|
|
640
|
+
return {
|
|
641
|
+
key: detail?.key ?? hotspot.key ?? "",
|
|
642
|
+
rule: hotspot.ruleKey ?? rule?.key ?? "",
|
|
643
|
+
ruleName: normalizeText(rule?.name),
|
|
644
|
+
status: detail?.status ?? hotspot.status ?? "UNKNOWN",
|
|
645
|
+
message: detail?.message ?? hotspot.message ?? "",
|
|
646
|
+
path: detail?.component?.path ?? resolveSonarPath(hotspot.component, components),
|
|
647
|
+
line: detail?.line ?? detail?.textRange?.startLine ?? hotspot.line ?? hotspot.textRange?.startLine ?? null,
|
|
648
|
+
securityCategory: rule?.securityCategory ?? hotspot.securityCategory,
|
|
649
|
+
vulnerabilityProbability: rule?.vulnerabilityProbability ?? hotspot.vulnerabilityProbability,
|
|
650
|
+
riskDescription: normalizeText(rule?.riskDescription),
|
|
651
|
+
vulnerabilityDescription: normalizeText(rule?.vulnerabilityDescription),
|
|
652
|
+
fixRecommendations: normalizeText(rule?.fixRecommendations)
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
async function fetchSonarJson(apiUrl) {
|
|
492
656
|
const controller = new AbortController();
|
|
493
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
657
|
+
const timeoutId = setTimeout(() => controller.abort(), SONAR_FETCH_TIMEOUT_MS);
|
|
494
658
|
try {
|
|
495
659
|
const response = await fetch(apiUrl, { signal: controller.signal });
|
|
496
|
-
if (!response.ok)
|
|
497
|
-
|
|
498
|
-
|
|
660
|
+
if (!response.ok) {
|
|
661
|
+
return { ok: false, status: response.status };
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
ok: true,
|
|
665
|
+
status: response.status,
|
|
666
|
+
data: await response.json()
|
|
667
|
+
};
|
|
499
668
|
} catch {
|
|
500
|
-
return null;
|
|
669
|
+
return { ok: false, status: null };
|
|
501
670
|
} finally {
|
|
502
671
|
clearTimeout(timeoutId);
|
|
503
672
|
}
|
|
504
673
|
}
|
|
674
|
+
async function fetchSonarNewIssues(projectKey, prNumber) {
|
|
675
|
+
const apiUrl = `https://sonarcloud.io/api/issues/search?componentKeys=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&resolved=false&ps=1`;
|
|
676
|
+
const response = await fetchSonarJson(apiUrl);
|
|
677
|
+
if (!response.ok) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
return response.data.paging?.total ?? null;
|
|
681
|
+
}
|
|
682
|
+
async function fetchSonarGateViolations(projectKey, prNumber) {
|
|
683
|
+
const apiUrl = `https://sonarcloud.io/api/qualitygates/project_status?projectKey=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}`;
|
|
684
|
+
const response = await fetchSonarJson(apiUrl);
|
|
685
|
+
if (!response.ok) {
|
|
686
|
+
return response;
|
|
687
|
+
}
|
|
688
|
+
const projectStatus = response.data.projectStatus;
|
|
689
|
+
const gateViolations = (projectStatus?.conditions ?? []).filter((condition) => condition.status !== void 0 && condition.status !== "OK").map((condition) => ({
|
|
690
|
+
metricKey: condition.metricKey ?? "unknown",
|
|
691
|
+
status: condition.status ?? "ERROR",
|
|
692
|
+
comparator: condition.comparator,
|
|
693
|
+
actualValue: condition.actualValue,
|
|
694
|
+
errorThreshold: condition.errorThreshold
|
|
695
|
+
}));
|
|
696
|
+
return {
|
|
697
|
+
ok: true,
|
|
698
|
+
status: response.status,
|
|
699
|
+
data: {
|
|
700
|
+
status: "available",
|
|
701
|
+
qualityGateStatus: projectStatus?.status,
|
|
702
|
+
gateViolations,
|
|
703
|
+
issues: [],
|
|
704
|
+
securityHotspots: []
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async function fetchSonarIssues(projectKey, prNumber) {
|
|
709
|
+
const pageSize = 100;
|
|
710
|
+
const issues = [];
|
|
711
|
+
const components = /* @__PURE__ */ new Map();
|
|
712
|
+
const rules = /* @__PURE__ */ new Map();
|
|
713
|
+
let page = 1;
|
|
714
|
+
let total = null;
|
|
715
|
+
while (true) {
|
|
716
|
+
const apiUrl = `https://sonarcloud.io/api/issues/search?componentKeys=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&resolved=false&ps=${String(pageSize)}&p=${String(page)}&additionalFields=_all`;
|
|
717
|
+
const response = await fetchSonarJson(apiUrl);
|
|
718
|
+
if (!response.ok) {
|
|
719
|
+
return response;
|
|
720
|
+
}
|
|
721
|
+
const paging = response.data.paging;
|
|
722
|
+
total ??= paging?.total ?? null;
|
|
723
|
+
mapSonarIssuesPage(response.data, issues, components, rules);
|
|
724
|
+
const fetchedCount = page * (paging?.pageSize ?? pageSize);
|
|
725
|
+
if (paging?.total === void 0 || fetchedCount >= paging.total) {
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
page += 1;
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
ok: true,
|
|
732
|
+
status: 200,
|
|
733
|
+
data: {
|
|
734
|
+
total,
|
|
735
|
+
issues
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
async function fetchSonarHotspotDetail(hotspotKey) {
|
|
740
|
+
const apiUrl = `https://sonarcloud.io/api/hotspots/show?hotspot=${encodeURIComponent(hotspotKey)}`;
|
|
741
|
+
return fetchSonarJson(apiUrl);
|
|
742
|
+
}
|
|
743
|
+
async function fetchSonarHotspots(projectKey, prNumber) {
|
|
744
|
+
const pageSize = 100;
|
|
745
|
+
const rawHotspots = [];
|
|
746
|
+
const components = /* @__PURE__ */ new Map();
|
|
747
|
+
let page = 1;
|
|
748
|
+
while (true) {
|
|
749
|
+
const apiUrl = `https://sonarcloud.io/api/hotspots/search?projectKey=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&onlyMine=false&sinceLeakPeriod=true&ps=${String(pageSize)}&p=${String(page)}`;
|
|
750
|
+
const response = await fetchSonarJson(apiUrl);
|
|
751
|
+
if (!response.ok) {
|
|
752
|
+
return response;
|
|
753
|
+
}
|
|
754
|
+
const paging = response.data.paging;
|
|
755
|
+
mapSonarHotspotsPage(response.data, rawHotspots, components);
|
|
756
|
+
const fetchedCount = page * (paging?.pageSize ?? pageSize);
|
|
757
|
+
if (paging?.total === void 0 || fetchedCount >= paging.total) {
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
page += 1;
|
|
761
|
+
}
|
|
762
|
+
const detailResults = await Promise.all(rawHotspots.map((hotspot) => fetchSonarHotspotDetail(hotspot.key ?? "")));
|
|
763
|
+
const securityHotspots = [];
|
|
764
|
+
for (let index = 0; index < rawHotspots.length; index += 1) {
|
|
765
|
+
const hotspot = rawHotspots[index];
|
|
766
|
+
const detailResult = detailResults[index];
|
|
767
|
+
if (detailResult && !detailResult.ok && detailResult.status === 401) {
|
|
768
|
+
return detailResult;
|
|
769
|
+
}
|
|
770
|
+
securityHotspots.push(mapSonarHotspot(hotspot, components, detailResult?.ok ? detailResult.data : void 0));
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
ok: true,
|
|
774
|
+
status: 200,
|
|
775
|
+
data: securityHotspots
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function sonarPrivateFailureSummary(sonarcloudUrl) {
|
|
779
|
+
return {
|
|
780
|
+
status: "private",
|
|
781
|
+
gateViolations: [],
|
|
782
|
+
issues: [],
|
|
783
|
+
securityHotspots: [],
|
|
784
|
+
privateMessage: `SonarCloud project is private. Open the Sonar URL in an authenticated browser: ${sonarcloudUrl}`
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
async function fetchSonarFailureSummary(projectKey, prNumber, sonarcloudUrl) {
|
|
788
|
+
const [gateResult, issuesResult, hotspotsResult] = await Promise.all([
|
|
789
|
+
fetchSonarGateViolations(projectKey, prNumber),
|
|
790
|
+
fetchSonarIssues(projectKey, prNumber),
|
|
791
|
+
fetchSonarHotspots(projectKey, prNumber)
|
|
792
|
+
]);
|
|
793
|
+
if (!gateResult.ok && gateResult.status === 401) {
|
|
794
|
+
return {
|
|
795
|
+
summary: sonarPrivateFailureSummary(sonarcloudUrl),
|
|
796
|
+
issueTotal: null
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (!issuesResult.ok && issuesResult.status === 401) {
|
|
800
|
+
return {
|
|
801
|
+
summary: sonarPrivateFailureSummary(sonarcloudUrl),
|
|
802
|
+
issueTotal: null
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
if (!hotspotsResult.ok && hotspotsResult.status === 401) {
|
|
806
|
+
return {
|
|
807
|
+
summary: sonarPrivateFailureSummary(sonarcloudUrl),
|
|
808
|
+
issueTotal: null
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
const gateViolations = gateResult.ok ? gateResult.data.gateViolations : [];
|
|
812
|
+
const issues = issuesResult.ok ? issuesResult.data.issues : [];
|
|
813
|
+
const securityHotspots = hotspotsResult.ok ? hotspotsResult.data : [];
|
|
814
|
+
const qualityGateStatus = gateResult.ok ? gateResult.data.qualityGateStatus : void 0;
|
|
815
|
+
const issueTotal = issuesResult.ok ? issuesResult.data.total : null;
|
|
816
|
+
if (gateViolations.length === 0 && issues.length === 0 && securityHotspots.length === 0 && !qualityGateStatus) {
|
|
817
|
+
return {
|
|
818
|
+
summary: {
|
|
819
|
+
status: "unavailable",
|
|
820
|
+
gateViolations: [],
|
|
821
|
+
issues: [],
|
|
822
|
+
securityHotspots: [],
|
|
823
|
+
unavailableMessage: "SonarCloud failure details are unavailable right now."
|
|
824
|
+
},
|
|
825
|
+
issueTotal
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
summary: {
|
|
830
|
+
status: "available",
|
|
831
|
+
qualityGateStatus,
|
|
832
|
+
gateViolations,
|
|
833
|
+
issues,
|
|
834
|
+
securityHotspots
|
|
835
|
+
},
|
|
836
|
+
issueTotal
|
|
837
|
+
};
|
|
838
|
+
}
|
|
505
839
|
async function getPrInfoGh(branch) {
|
|
506
840
|
const { stdout, stderr, status } = run2("gh", [
|
|
507
841
|
"pr",
|
|
@@ -532,21 +866,21 @@ async function getPrInfoGh(branch) {
|
|
|
532
866
|
detailsUrl: enriched?.detailsUrl || c.detailsUrl || ""
|
|
533
867
|
};
|
|
534
868
|
});
|
|
535
|
-
const sonarCheck = checks.find((c) =>
|
|
536
|
-
try {
|
|
537
|
-
const hostname = new URL(c.detailsUrl).hostname;
|
|
538
|
-
return hostname === "sonarcloud.io" || hostname.endsWith(".sonarcloud.io");
|
|
539
|
-
} catch {
|
|
540
|
-
return false;
|
|
541
|
-
}
|
|
542
|
-
});
|
|
869
|
+
const sonarCheck = checks.find((c) => isSonarUrl(c.detailsUrl));
|
|
543
870
|
let sonarcloudUrl;
|
|
544
871
|
let sonarNewIssues;
|
|
872
|
+
let sonarFailures;
|
|
545
873
|
if (sonarCheck) {
|
|
546
874
|
sonarcloudUrl = sonarCheck.detailsUrl;
|
|
547
875
|
const projectKey = extractSonarProjectKey(sonarCheck.detailsUrl);
|
|
548
876
|
if (projectKey) {
|
|
549
|
-
|
|
877
|
+
if (sonarCheck.conclusion !== null && SONAR_FAIL_CONCLUSIONS.has(sonarCheck.conclusion)) {
|
|
878
|
+
const sonarSummary = await fetchSonarFailureSummary(projectKey, raw.number, sonarcloudUrl);
|
|
879
|
+
sonarFailures = sonarSummary.summary;
|
|
880
|
+
sonarNewIssues = sonarSummary.issueTotal;
|
|
881
|
+
} else {
|
|
882
|
+
sonarNewIssues = await fetchSonarNewIssues(projectKey, raw.number);
|
|
883
|
+
}
|
|
550
884
|
} else {
|
|
551
885
|
sonarNewIssues = null;
|
|
552
886
|
}
|
|
@@ -557,7 +891,8 @@ async function getPrInfoGh(branch) {
|
|
|
557
891
|
state: raw.state,
|
|
558
892
|
url: raw.url,
|
|
559
893
|
checks,
|
|
560
|
-
...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues }
|
|
894
|
+
...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues },
|
|
895
|
+
...sonarFailures === void 0 ? {} : { sonarFailures }
|
|
561
896
|
};
|
|
562
897
|
}
|
|
563
898
|
async function getPrInfo2(branch) {
|
|
@@ -767,7 +1102,7 @@ function formatChecks(checks) {
|
|
|
767
1102
|
return lines.join("\n") + "\n";
|
|
768
1103
|
}
|
|
769
1104
|
function sleep(ms) {
|
|
770
|
-
return new Promise((
|
|
1105
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
771
1106
|
}
|
|
772
1107
|
function isSonarCheck(check) {
|
|
773
1108
|
try {
|
|
@@ -789,6 +1124,103 @@ function formatFailedChecks(failed) {
|
|
|
789
1124
|
}
|
|
790
1125
|
return lines.join("\n") + "\n";
|
|
791
1126
|
}
|
|
1127
|
+
function formatGateViolation(violation) {
|
|
1128
|
+
const parts = [sanitizeText(violation.metricKey)];
|
|
1129
|
+
if (violation.actualValue) parts.push(`actual ${sanitizeText(violation.actualValue)}`);
|
|
1130
|
+
if (violation.comparator && violation.errorThreshold) {
|
|
1131
|
+
parts.push(`${sanitizeText(violation.comparator)} ${sanitizeText(violation.errorThreshold)}`);
|
|
1132
|
+
} else if (violation.errorThreshold) {
|
|
1133
|
+
parts.push(`threshold ${sanitizeText(violation.errorThreshold)}`);
|
|
1134
|
+
}
|
|
1135
|
+
return parts.join(" | ");
|
|
1136
|
+
}
|
|
1137
|
+
function formatLocation(path, line) {
|
|
1138
|
+
if (!path) return void 0;
|
|
1139
|
+
const safePath = sanitizeText(path);
|
|
1140
|
+
if (!line) return safePath;
|
|
1141
|
+
return `${safePath}:${String(line)}`;
|
|
1142
|
+
}
|
|
1143
|
+
function formatRuleWithName(rule, ruleName) {
|
|
1144
|
+
const safeRule = sanitizeText(rule);
|
|
1145
|
+
if (!ruleName) return safeRule;
|
|
1146
|
+
return `${safeRule} (${sanitizeText(ruleName)})`;
|
|
1147
|
+
}
|
|
1148
|
+
function formatSonarIssue(issue) {
|
|
1149
|
+
const lines = [` - ${sanitizeText(issue.message)}`];
|
|
1150
|
+
const location = formatLocation(issue.path, issue.line);
|
|
1151
|
+
if (location) lines.push(` Location: ${location}`);
|
|
1152
|
+
if (issue.severity || issue.type) {
|
|
1153
|
+
const labels = [issue.severity, issue.type].filter((label) => Boolean(label)).map(sanitizeText).join(" / ");
|
|
1154
|
+
lines.push(` Classification: ${labels}`);
|
|
1155
|
+
}
|
|
1156
|
+
if (issue.rule) lines.push(` Rule: ${sanitizeText(issue.rule)}`);
|
|
1157
|
+
if (issue.explanation) lines.push(` Explanation: ${sanitizeText(issue.explanation)}`);
|
|
1158
|
+
return lines;
|
|
1159
|
+
}
|
|
1160
|
+
function formatSonarHotspot(hotspot) {
|
|
1161
|
+
const lines = [` - ${sanitizeText(hotspot.message)}`];
|
|
1162
|
+
const location = formatLocation(hotspot.path, hotspot.line);
|
|
1163
|
+
if (location) lines.push(` Location: ${location}`);
|
|
1164
|
+
if (hotspot.status) lines.push(` Status: ${sanitizeText(hotspot.status)}`);
|
|
1165
|
+
if (hotspot.vulnerabilityProbability || hotspot.securityCategory) {
|
|
1166
|
+
const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((label) => Boolean(label)).map(sanitizeText).join(" / ");
|
|
1167
|
+
lines.push(` Classification: ${labels}`);
|
|
1168
|
+
}
|
|
1169
|
+
if (hotspot.rule) {
|
|
1170
|
+
lines.push(` Rule: ${formatRuleWithName(hotspot.rule, hotspot.ruleName)}`);
|
|
1171
|
+
}
|
|
1172
|
+
if (hotspot.riskDescription) lines.push(` Risk: ${sanitizeText(hotspot.riskDescription)}`);
|
|
1173
|
+
if (hotspot.vulnerabilityDescription) lines.push(` Review: ${sanitizeText(hotspot.vulnerabilityDescription)}`);
|
|
1174
|
+
if (hotspot.fixRecommendations) lines.push(` Fix: ${sanitizeText(hotspot.fixRecommendations)}`);
|
|
1175
|
+
return lines;
|
|
1176
|
+
}
|
|
1177
|
+
function appendGateViolations(lines, gateViolations) {
|
|
1178
|
+
if (gateViolations.length === 0) return;
|
|
1179
|
+
lines.push(" Gate Violations:");
|
|
1180
|
+
for (const violation of gateViolations) {
|
|
1181
|
+
lines.push(` - ${formatGateViolation(violation)}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function appendSonarIssues(lines, issues) {
|
|
1185
|
+
if (issues.length === 0) return;
|
|
1186
|
+
lines.push(" Issues:");
|
|
1187
|
+
for (const issue of issues) {
|
|
1188
|
+
lines.push(...formatSonarIssue(issue));
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function appendSecurityHotspots(lines, securityHotspots) {
|
|
1192
|
+
if (securityHotspots.length === 0) return;
|
|
1193
|
+
lines.push(" Security Hotspots:");
|
|
1194
|
+
for (const hotspot of securityHotspots) {
|
|
1195
|
+
lines.push(...formatSonarHotspot(hotspot));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function hasSonarFailureDetails(sonarFailures) {
|
|
1199
|
+
return Boolean(sonarFailures.qualityGateStatus) || sonarFailures.gateViolations.length > 0 || sonarFailures.issues.length > 0 || sonarFailures.securityHotspots.length > 0;
|
|
1200
|
+
}
|
|
1201
|
+
function formatSonarFailures(sonarFailures, sonarcloudUrl) {
|
|
1202
|
+
const lines = ["Sonar Failures:"];
|
|
1203
|
+
if (sonarFailures.status === "private") {
|
|
1204
|
+
lines.push(` Note: ${sanitizeText(sonarFailures.privateMessage ?? "SonarCloud project is private.")}`);
|
|
1205
|
+
return lines.join("\n") + "\n";
|
|
1206
|
+
}
|
|
1207
|
+
if (sonarFailures.status === "unavailable") {
|
|
1208
|
+
lines.push(` Note: ${sanitizeText(sonarFailures.unavailableMessage ?? "SonarCloud failure details are unavailable.")}`);
|
|
1209
|
+
if (sonarcloudUrl) lines.push(` URL: ${sonarcloudUrl}`);
|
|
1210
|
+
return lines.join("\n") + "\n";
|
|
1211
|
+
}
|
|
1212
|
+
if (sonarFailures.qualityGateStatus) {
|
|
1213
|
+
lines.push(` Quality Gate: ${sanitizeText(sonarFailures.qualityGateStatus)}`);
|
|
1214
|
+
}
|
|
1215
|
+
appendGateViolations(lines, sonarFailures.gateViolations);
|
|
1216
|
+
appendSonarIssues(lines, sonarFailures.issues);
|
|
1217
|
+
appendSecurityHotspots(lines, sonarFailures.securityHotspots);
|
|
1218
|
+
if (!hasSonarFailureDetails(sonarFailures)) {
|
|
1219
|
+
lines.push(" Note: SonarCloud reported a failure but returned no violation, issue, or hotspot details.");
|
|
1220
|
+
if (sonarcloudUrl) lines.push(` URL: ${sonarcloudUrl}`);
|
|
1221
|
+
}
|
|
1222
|
+
return lines.join("\n") + "\n";
|
|
1223
|
+
}
|
|
792
1224
|
var POLL_INTERVAL_MS = 1e4;
|
|
793
1225
|
var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").option("--wait-finish-checks", "Poll until all checks complete, then print the normal get-pr-info output").addHelpText(
|
|
794
1226
|
"after",
|
|
@@ -864,6 +1296,9 @@ URL: ${pr.url}
|
|
|
864
1296
|
if (failed.length > 0) {
|
|
865
1297
|
process.stdout.write(formatFailedChecks(failed));
|
|
866
1298
|
}
|
|
1299
|
+
if (pr.sonarFailures !== void 0) {
|
|
1300
|
+
process.stdout.write(formatSonarFailures(pr.sonarFailures, pr.sonarcloudUrl));
|
|
1301
|
+
}
|
|
867
1302
|
}
|
|
868
1303
|
});
|
|
869
1304
|
var ANSI_ESCAPE_RE = new RegExp("\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "g");
|
|
@@ -1138,7 +1573,7 @@ function postComment(issueNumber, body) {
|
|
|
1138
1573
|
import { spawn, spawnSync as spawnSync4 } from "child_process";
|
|
1139
1574
|
import { createInterface } from "readline";
|
|
1140
1575
|
import { existsSync } from "fs";
|
|
1141
|
-
import { delimiter, join as
|
|
1576
|
+
import { delimiter, join as join3 } from "path";
|
|
1142
1577
|
|
|
1143
1578
|
// src/cli/spawnUtils.ts
|
|
1144
1579
|
function truncate(str, max) {
|
|
@@ -1173,7 +1608,7 @@ function handleExitCode(status, toolName) {
|
|
|
1173
1608
|
function resolveCommand(name) {
|
|
1174
1609
|
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
1175
1610
|
for (const dir of pathDirs) {
|
|
1176
|
-
const candidate =
|
|
1611
|
+
const candidate = join3(dir, name);
|
|
1177
1612
|
if (existsSync(candidate)) return candidate;
|
|
1178
1613
|
}
|
|
1179
1614
|
return name;
|
|
@@ -1209,7 +1644,7 @@ function invokeClaudeCodeSync(prompt, yolo, model) {
|
|
|
1209
1644
|
handleExitCode(result.status, "Claude Code");
|
|
1210
1645
|
}
|
|
1211
1646
|
function invokeClaudeCodeVerbose(prompt, yolo, model) {
|
|
1212
|
-
return new Promise((
|
|
1647
|
+
return new Promise((resolve3) => {
|
|
1213
1648
|
const claudeBin = resolveCommand("claude");
|
|
1214
1649
|
const args = [];
|
|
1215
1650
|
if (yolo) args.push("--dangerously-skip-permissions");
|
|
@@ -1231,7 +1666,7 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
|
|
|
1231
1666
|
});
|
|
1232
1667
|
child.on("close", (code) => {
|
|
1233
1668
|
handleExitCode(code, "Claude Code");
|
|
1234
|
-
|
|
1669
|
+
resolve3();
|
|
1235
1670
|
});
|
|
1236
1671
|
});
|
|
1237
1672
|
}
|
|
@@ -1403,6 +1838,9 @@ function withPush(prompt, push) {
|
|
|
1403
1838
|
|
|
1404
1839
|
${PUSH_INSTRUCTION}` : prompt;
|
|
1405
1840
|
}
|
|
1841
|
+
function formatPrInfoContext(pr) {
|
|
1842
|
+
return JSON.stringify(pr, null, 2);
|
|
1843
|
+
}
|
|
1406
1844
|
function addAiOptions(cmd) {
|
|
1407
1845
|
return cmd.option("--codex", "Use Codex CLI instead of Claude Code").option("--verbose", "Show step-by-step progress (Claude only; ignored for Codex)").option("--push", "Append instruction to commit and push changes after the AI finishes").option("--opus", "Use claude-opus-4-6 (Claude only)").option("--sonnet", "Use claude-sonnet-4-6 (Claude only)").option("--haiku", "Use claude-haiku-4-5-20251001 (Claude only)");
|
|
1408
1846
|
}
|
|
@@ -1444,7 +1882,10 @@ var executeSonarCmd = addAiOptions(
|
|
|
1444
1882
|
const fullPrompt = withPush(
|
|
1445
1883
|
`${sonarPromptText}
|
|
1446
1884
|
|
|
1447
|
-
SonarCloud analysis URL: ${pr.sonarcloudUrl}
|
|
1885
|
+
SonarCloud analysis URL: ${pr.sonarcloudUrl}
|
|
1886
|
+
|
|
1887
|
+
Current PR context from automata git get-pr-info --json:
|
|
1888
|
+
${formatPrInfoContext(pr)}`,
|
|
1448
1889
|
options.push
|
|
1449
1890
|
);
|
|
1450
1891
|
if (options.codex) {
|