automata-cli 0.1.0-bugfix-automata-deletion.94 → 0.1.0-feature-021-execute-command.127
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 +14 -0
- package/dist/index.js +669 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,6 +99,20 @@ See [docs/implement-next.md](docs/implement-next.md) for full details.
|
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
102
|
+
## `automata execute`
|
|
103
|
+
|
|
104
|
+
Delegate work to an AI executor (Claude or Codex) by providing a prompt inline, from a file, or via stdin.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
automata execute --with claude --prompt "refactor the auth module"
|
|
108
|
+
automata execute --with codex --file-prompt prompts/fix.md
|
|
109
|
+
echo "fix lint errors" | automata execute --with claude
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See [docs/execute.md](docs/execute.md) for full details.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
102
116
|
## Development
|
|
103
117
|
|
|
104
118
|
### Prerequisites
|
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,314 @@ function extractSonarProjectKey(url) {
|
|
|
487
557
|
return null;
|
|
488
558
|
}
|
|
489
559
|
}
|
|
490
|
-
|
|
491
|
-
|
|
560
|
+
function buildSonarPullRequestUrl(projectKey, prNumber) {
|
|
561
|
+
return `https://sonarcloud.io/summary/new_code?id=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}`;
|
|
562
|
+
}
|
|
563
|
+
function resolveSonarPullRequestUrl(url, prNumber) {
|
|
564
|
+
const projectKey = extractSonarProjectKey(url);
|
|
565
|
+
if (!projectKey) return url;
|
|
566
|
+
return buildSonarPullRequestUrl(projectKey, prNumber);
|
|
567
|
+
}
|
|
568
|
+
function isSonarUrl(url) {
|
|
569
|
+
try {
|
|
570
|
+
const hostname = new URL(url).hostname;
|
|
571
|
+
return hostname === "sonarcloud.io" || hostname.endsWith(".sonarcloud.io");
|
|
572
|
+
} catch {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function stripHtml(text) {
|
|
577
|
+
let stripped = "";
|
|
578
|
+
let inTag = false;
|
|
579
|
+
for (const char of text) {
|
|
580
|
+
if (char === "<") {
|
|
581
|
+
inTag = true;
|
|
582
|
+
stripped += " ";
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (char === ">") {
|
|
586
|
+
inTag = false;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (!inTag) {
|
|
590
|
+
stripped += char;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return stripped;
|
|
594
|
+
}
|
|
595
|
+
function normalizeText(text) {
|
|
596
|
+
if (!text) return void 0;
|
|
597
|
+
const normalized = stripHtml(text).replaceAll(/\s+/g, " ").trim();
|
|
598
|
+
return normalized || void 0;
|
|
599
|
+
}
|
|
600
|
+
function resolveSonarPath(componentKey, components) {
|
|
601
|
+
if (!componentKey) return void 0;
|
|
602
|
+
const mapped = components.get(componentKey);
|
|
603
|
+
if (mapped) return mapped;
|
|
604
|
+
const separatorIndex = componentKey.indexOf(":");
|
|
605
|
+
if (separatorIndex === -1) return void 0;
|
|
606
|
+
const path = componentKey.slice(separatorIndex + 1).trim();
|
|
607
|
+
return path || void 0;
|
|
608
|
+
}
|
|
609
|
+
function mapSonarIssuesPage(data, targetIssues, components, rules) {
|
|
610
|
+
for (const component of data.components ?? []) {
|
|
611
|
+
if (component.key && component.path) {
|
|
612
|
+
components.set(component.key, component.path);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const rule of data.rules ?? []) {
|
|
616
|
+
if (!rule.key) continue;
|
|
617
|
+
const explanation = normalizeText(rule.htmlDesc) ?? normalizeText(rule.htmlNote) ?? normalizeText(rule.name);
|
|
618
|
+
if (explanation) {
|
|
619
|
+
rules.set(rule.key, explanation);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const issue of data.issues ?? []) {
|
|
623
|
+
if (!issue.key || !issue.message) continue;
|
|
624
|
+
const ruleKey = issue.rule;
|
|
625
|
+
const mappedIssue = {
|
|
626
|
+
key: issue.key,
|
|
627
|
+
severity: issue.severity,
|
|
628
|
+
type: issue.type,
|
|
629
|
+
message: issue.message,
|
|
630
|
+
path: resolveSonarPath(issue.component, components),
|
|
631
|
+
line: issue.line ?? issue.textRange?.startLine ?? null,
|
|
632
|
+
...ruleKey ? { rule: ruleKey } : {},
|
|
633
|
+
...ruleKey && rules.has(ruleKey) ? { explanation: rules.get(ruleKey) } : {}
|
|
634
|
+
};
|
|
635
|
+
targetIssues.push(mappedIssue);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function mapSonarHotspotsPage(data, targetHotspots, components) {
|
|
639
|
+
for (const component of data.components ?? []) {
|
|
640
|
+
if (component.key && component.path) {
|
|
641
|
+
components.set(component.key, component.path);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
targetHotspots.push(...data.hotspots ?? []);
|
|
645
|
+
}
|
|
646
|
+
function mapSonarHotspot(hotspot, components, detail) {
|
|
647
|
+
const rule = detail?.rule;
|
|
648
|
+
return {
|
|
649
|
+
key: detail?.key ?? hotspot.key ?? "",
|
|
650
|
+
rule: hotspot.ruleKey ?? rule?.key ?? "",
|
|
651
|
+
ruleName: normalizeText(rule?.name),
|
|
652
|
+
status: detail?.status ?? hotspot.status ?? "UNKNOWN",
|
|
653
|
+
message: detail?.message ?? hotspot.message ?? "",
|
|
654
|
+
path: detail?.component?.path ?? resolveSonarPath(hotspot.component, components),
|
|
655
|
+
line: detail?.line ?? detail?.textRange?.startLine ?? hotspot.line ?? hotspot.textRange?.startLine ?? null,
|
|
656
|
+
securityCategory: rule?.securityCategory ?? hotspot.securityCategory,
|
|
657
|
+
vulnerabilityProbability: rule?.vulnerabilityProbability ?? hotspot.vulnerabilityProbability,
|
|
658
|
+
riskDescription: normalizeText(rule?.riskDescription),
|
|
659
|
+
vulnerabilityDescription: normalizeText(rule?.vulnerabilityDescription),
|
|
660
|
+
fixRecommendations: normalizeText(rule?.fixRecommendations)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
async function fetchSonarJson(apiUrl) {
|
|
492
664
|
const controller = new AbortController();
|
|
493
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
665
|
+
const timeoutId = setTimeout(() => controller.abort(), SONAR_FETCH_TIMEOUT_MS);
|
|
494
666
|
try {
|
|
495
667
|
const response = await fetch(apiUrl, { signal: controller.signal });
|
|
496
|
-
if (!response.ok)
|
|
497
|
-
|
|
498
|
-
|
|
668
|
+
if (!response.ok) {
|
|
669
|
+
return { ok: false, status: response.status };
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
ok: true,
|
|
673
|
+
status: response.status,
|
|
674
|
+
data: await response.json()
|
|
675
|
+
};
|
|
499
676
|
} catch {
|
|
500
|
-
return null;
|
|
677
|
+
return { ok: false, status: null };
|
|
501
678
|
} finally {
|
|
502
679
|
clearTimeout(timeoutId);
|
|
503
680
|
}
|
|
504
681
|
}
|
|
682
|
+
async function fetchSonarNewIssues(projectKey, prNumber, sonarcloudUrl) {
|
|
683
|
+
const apiUrl = `https://sonarcloud.io/api/issues/search?componentKeys=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&resolved=false&ps=1`;
|
|
684
|
+
const response = await fetchSonarJson(apiUrl);
|
|
685
|
+
if (!response.ok) {
|
|
686
|
+
if (response.status === 401) {
|
|
687
|
+
return {
|
|
688
|
+
total: null,
|
|
689
|
+
note: `SonarCloud project is private. Open the Sonar URL in an authenticated browser: ${sonarcloudUrl}`
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
total: null,
|
|
694
|
+
note: "SonarCloud new-issue count is unavailable right now."
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (typeof response.data.paging?.total === "number") {
|
|
698
|
+
return { total: response.data.paging.total };
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
total: null,
|
|
702
|
+
note: "SonarCloud did not return a new-issue count for this pull request."
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
async function fetchSonarGateViolations(projectKey, prNumber) {
|
|
706
|
+
const apiUrl = `https://sonarcloud.io/api/qualitygates/project_status?projectKey=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}`;
|
|
707
|
+
const response = await fetchSonarJson(apiUrl);
|
|
708
|
+
if (!response.ok) {
|
|
709
|
+
return response;
|
|
710
|
+
}
|
|
711
|
+
const projectStatus = response.data.projectStatus;
|
|
712
|
+
const gateViolations = (projectStatus?.conditions ?? []).filter((condition) => condition.status !== void 0 && condition.status !== "OK").map((condition) => ({
|
|
713
|
+
metricKey: condition.metricKey ?? "unknown",
|
|
714
|
+
status: condition.status ?? "ERROR",
|
|
715
|
+
comparator: condition.comparator,
|
|
716
|
+
actualValue: condition.actualValue,
|
|
717
|
+
errorThreshold: condition.errorThreshold
|
|
718
|
+
}));
|
|
719
|
+
return {
|
|
720
|
+
ok: true,
|
|
721
|
+
status: response.status,
|
|
722
|
+
data: {
|
|
723
|
+
status: "available",
|
|
724
|
+
qualityGateStatus: projectStatus?.status,
|
|
725
|
+
gateViolations,
|
|
726
|
+
issues: [],
|
|
727
|
+
securityHotspots: []
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
async function fetchSonarIssues(projectKey, prNumber) {
|
|
732
|
+
const pageSize = 100;
|
|
733
|
+
const issues = [];
|
|
734
|
+
const components = /* @__PURE__ */ new Map();
|
|
735
|
+
const rules = /* @__PURE__ */ new Map();
|
|
736
|
+
let page = 1;
|
|
737
|
+
let total = null;
|
|
738
|
+
while (true) {
|
|
739
|
+
const apiUrl = `https://sonarcloud.io/api/issues/search?componentKeys=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&resolved=false&ps=${String(pageSize)}&p=${String(page)}&additionalFields=_all`;
|
|
740
|
+
const response = await fetchSonarJson(apiUrl);
|
|
741
|
+
if (!response.ok) {
|
|
742
|
+
return response;
|
|
743
|
+
}
|
|
744
|
+
const paging = response.data.paging;
|
|
745
|
+
total ??= paging?.total ?? null;
|
|
746
|
+
mapSonarIssuesPage(response.data, issues, components, rules);
|
|
747
|
+
const fetchedCount = page * (paging?.pageSize ?? pageSize);
|
|
748
|
+
if (paging?.total === void 0 || fetchedCount >= paging.total) {
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
page += 1;
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
ok: true,
|
|
755
|
+
status: 200,
|
|
756
|
+
data: {
|
|
757
|
+
total,
|
|
758
|
+
issues
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
async function fetchSonarHotspotDetail(hotspotKey) {
|
|
763
|
+
const apiUrl = `https://sonarcloud.io/api/hotspots/show?hotspot=${encodeURIComponent(hotspotKey)}`;
|
|
764
|
+
return fetchSonarJson(apiUrl);
|
|
765
|
+
}
|
|
766
|
+
async function fetchSonarHotspots(projectKey, prNumber) {
|
|
767
|
+
const pageSize = 100;
|
|
768
|
+
const rawHotspots = [];
|
|
769
|
+
const components = /* @__PURE__ */ new Map();
|
|
770
|
+
let page = 1;
|
|
771
|
+
while (true) {
|
|
772
|
+
const apiUrl = `https://sonarcloud.io/api/hotspots/search?projectKey=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&onlyMine=false&sinceLeakPeriod=true&ps=${String(pageSize)}&p=${String(page)}`;
|
|
773
|
+
const response = await fetchSonarJson(apiUrl);
|
|
774
|
+
if (!response.ok) {
|
|
775
|
+
return response;
|
|
776
|
+
}
|
|
777
|
+
const paging = response.data.paging;
|
|
778
|
+
mapSonarHotspotsPage(response.data, rawHotspots, components);
|
|
779
|
+
const fetchedCount = page * (paging?.pageSize ?? pageSize);
|
|
780
|
+
if (paging?.total === void 0 || fetchedCount >= paging.total) {
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
page += 1;
|
|
784
|
+
}
|
|
785
|
+
const detailResults = await Promise.all(rawHotspots.map((hotspot) => fetchSonarHotspotDetail(hotspot.key ?? "")));
|
|
786
|
+
const securityHotspots = [];
|
|
787
|
+
for (let index = 0; index < rawHotspots.length; index += 1) {
|
|
788
|
+
const hotspot = rawHotspots[index];
|
|
789
|
+
const detailResult = detailResults[index];
|
|
790
|
+
if (detailResult && !detailResult.ok && detailResult.status === 401) {
|
|
791
|
+
return detailResult;
|
|
792
|
+
}
|
|
793
|
+
securityHotspots.push(mapSonarHotspot(hotspot, components, detailResult?.ok ? detailResult.data : void 0));
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
ok: true,
|
|
797
|
+
status: 200,
|
|
798
|
+
data: securityHotspots
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function sonarPrivateFailureSummary(sonarcloudUrl) {
|
|
802
|
+
return {
|
|
803
|
+
status: "private",
|
|
804
|
+
gateViolations: [],
|
|
805
|
+
issues: [],
|
|
806
|
+
securityHotspots: [],
|
|
807
|
+
privateMessage: `SonarCloud project is private. Open the Sonar URL in an authenticated browser: ${sonarcloudUrl}`
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
async function fetchSonarFailureSummary(projectKey, prNumber, sonarcloudUrl) {
|
|
811
|
+
const [gateResult, issuesResult, hotspotsResult] = await Promise.all([
|
|
812
|
+
fetchSonarGateViolations(projectKey, prNumber),
|
|
813
|
+
fetchSonarIssues(projectKey, prNumber),
|
|
814
|
+
fetchSonarHotspots(projectKey, prNumber)
|
|
815
|
+
]);
|
|
816
|
+
if (!gateResult.ok && gateResult.status === 401) {
|
|
817
|
+
return {
|
|
818
|
+
summary: sonarPrivateFailureSummary(sonarcloudUrl),
|
|
819
|
+
issueTotal: null,
|
|
820
|
+
issueNote: `SonarCloud project is private. Open the Sonar URL in an authenticated browser: ${sonarcloudUrl}`
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
if (!issuesResult.ok && issuesResult.status === 401) {
|
|
824
|
+
return {
|
|
825
|
+
summary: sonarPrivateFailureSummary(sonarcloudUrl),
|
|
826
|
+
issueTotal: null,
|
|
827
|
+
issueNote: `SonarCloud project is private. Open the Sonar URL in an authenticated browser: ${sonarcloudUrl}`
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (!hotspotsResult.ok && hotspotsResult.status === 401) {
|
|
831
|
+
return {
|
|
832
|
+
summary: sonarPrivateFailureSummary(sonarcloudUrl),
|
|
833
|
+
issueTotal: null,
|
|
834
|
+
issueNote: `SonarCloud project is private. Open the Sonar URL in an authenticated browser: ${sonarcloudUrl}`
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
const gateViolations = gateResult.ok ? gateResult.data.gateViolations : [];
|
|
838
|
+
const issues = issuesResult.ok ? issuesResult.data.issues : [];
|
|
839
|
+
const securityHotspots = hotspotsResult.ok ? hotspotsResult.data : [];
|
|
840
|
+
const qualityGateStatus = gateResult.ok ? gateResult.data.qualityGateStatus : void 0;
|
|
841
|
+
const issueTotal = issuesResult.ok ? issuesResult.data.total : null;
|
|
842
|
+
const issueNote = issuesResult.ok ? void 0 : "SonarCloud new-issue count is unavailable right now.";
|
|
843
|
+
if (gateViolations.length === 0 && issues.length === 0 && securityHotspots.length === 0 && !qualityGateStatus) {
|
|
844
|
+
return {
|
|
845
|
+
summary: {
|
|
846
|
+
status: "unavailable",
|
|
847
|
+
gateViolations: [],
|
|
848
|
+
issues: [],
|
|
849
|
+
securityHotspots: [],
|
|
850
|
+
unavailableMessage: "SonarCloud failure details are unavailable right now."
|
|
851
|
+
},
|
|
852
|
+
issueTotal,
|
|
853
|
+
issueNote
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
summary: {
|
|
858
|
+
status: "available",
|
|
859
|
+
qualityGateStatus,
|
|
860
|
+
gateViolations,
|
|
861
|
+
issues,
|
|
862
|
+
securityHotspots
|
|
863
|
+
},
|
|
864
|
+
issueTotal,
|
|
865
|
+
issueNote
|
|
866
|
+
};
|
|
867
|
+
}
|
|
505
868
|
async function getPrInfoGh(branch) {
|
|
506
869
|
const { stdout, stderr, status } = run2("gh", [
|
|
507
870
|
"pr",
|
|
@@ -517,12 +880,16 @@ async function getPrInfoGh(branch) {
|
|
|
517
880
|
throw new Error(stderr.trim() || "Failed to query GitHub. Is `gh` installed and authenticated?");
|
|
518
881
|
}
|
|
519
882
|
const raw = JSON.parse(stdout);
|
|
520
|
-
const
|
|
883
|
+
const statusChecks = raw.statusCheckRollup ?? [];
|
|
884
|
+
const failedChecks = statusChecks.filter(
|
|
521
885
|
(c) => c.conclusion !== null && ["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"].includes(c.conclusion)
|
|
522
886
|
);
|
|
523
|
-
const
|
|
887
|
+
const sonarNeedsCheckRunOutput = statusChecks.some(
|
|
888
|
+
(c) => (isSonarUrl(c.detailsUrl) || c.name.toLowerCase().includes("sonar")) && !extractSonarProjectKey(c.detailsUrl)
|
|
889
|
+
);
|
|
890
|
+
const ownerRepo = failedChecks.length > 0 || sonarNeedsCheckRunOutput ? parseOwnerRepo() : null;
|
|
524
891
|
const checkOutputs = ownerRepo ? fetchCheckRunOutputs(ownerRepo, raw.headRefOid) : /* @__PURE__ */ new Map();
|
|
525
|
-
const checks =
|
|
892
|
+
const checks = statusChecks.map((c) => {
|
|
526
893
|
const enriched = checkOutputs.get(c.name);
|
|
527
894
|
return {
|
|
528
895
|
name: c.name,
|
|
@@ -532,23 +899,28 @@ async function getPrInfoGh(branch) {
|
|
|
532
899
|
detailsUrl: enriched?.detailsUrl || c.detailsUrl || ""
|
|
533
900
|
};
|
|
534
901
|
});
|
|
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
|
-
});
|
|
902
|
+
const sonarCheck = checks.find((c) => isSonarUrl(c.detailsUrl));
|
|
543
903
|
let sonarcloudUrl;
|
|
544
904
|
let sonarNewIssues;
|
|
905
|
+
let sonarNewIssuesNote;
|
|
906
|
+
let sonarFailures;
|
|
545
907
|
if (sonarCheck) {
|
|
546
|
-
sonarcloudUrl = sonarCheck.detailsUrl;
|
|
547
|
-
const projectKey = extractSonarProjectKey(
|
|
908
|
+
sonarcloudUrl = resolveSonarPullRequestUrl(sonarCheck.detailsUrl, raw.number);
|
|
909
|
+
const projectKey = extractSonarProjectKey(sonarcloudUrl);
|
|
548
910
|
if (projectKey) {
|
|
549
|
-
|
|
911
|
+
if (sonarCheck.conclusion !== null && SONAR_FAIL_CONCLUSIONS.has(sonarCheck.conclusion)) {
|
|
912
|
+
const sonarSummary = await fetchSonarFailureSummary(projectKey, raw.number, sonarcloudUrl);
|
|
913
|
+
sonarFailures = sonarSummary.summary;
|
|
914
|
+
sonarNewIssues = sonarSummary.issueTotal;
|
|
915
|
+
sonarNewIssuesNote = sonarSummary.issueNote;
|
|
916
|
+
} else {
|
|
917
|
+
const sonarNewIssuesResult = await fetchSonarNewIssues(projectKey, raw.number, sonarcloudUrl);
|
|
918
|
+
sonarNewIssues = sonarNewIssuesResult.total;
|
|
919
|
+
sonarNewIssuesNote = sonarNewIssuesResult.note;
|
|
920
|
+
}
|
|
550
921
|
} else {
|
|
551
922
|
sonarNewIssues = null;
|
|
923
|
+
sonarNewIssuesNote = "Could not determine the SonarCloud project key from the check URL.";
|
|
552
924
|
}
|
|
553
925
|
}
|
|
554
926
|
return {
|
|
@@ -557,7 +929,8 @@ async function getPrInfoGh(branch) {
|
|
|
557
929
|
state: raw.state,
|
|
558
930
|
url: raw.url,
|
|
559
931
|
checks,
|
|
560
|
-
...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues }
|
|
932
|
+
...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues, sonarNewIssuesNote },
|
|
933
|
+
...sonarFailures === void 0 ? {} : { sonarFailures }
|
|
561
934
|
};
|
|
562
935
|
}
|
|
563
936
|
async function getPrInfo2(branch) {
|
|
@@ -767,7 +1140,7 @@ function formatChecks(checks) {
|
|
|
767
1140
|
return lines.join("\n") + "\n";
|
|
768
1141
|
}
|
|
769
1142
|
function sleep(ms) {
|
|
770
|
-
return new Promise((
|
|
1143
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
771
1144
|
}
|
|
772
1145
|
function isSonarCheck(check) {
|
|
773
1146
|
try {
|
|
@@ -789,6 +1162,109 @@ function formatFailedChecks(failed) {
|
|
|
789
1162
|
}
|
|
790
1163
|
return lines.join("\n") + "\n";
|
|
791
1164
|
}
|
|
1165
|
+
function formatGateViolation(violation) {
|
|
1166
|
+
const parts = [sanitizeText(violation.metricKey)];
|
|
1167
|
+
if (violation.actualValue) parts.push(`actual ${sanitizeText(violation.actualValue)}`);
|
|
1168
|
+
if (violation.comparator && violation.errorThreshold) {
|
|
1169
|
+
parts.push(`${sanitizeText(violation.comparator)} ${sanitizeText(violation.errorThreshold)}`);
|
|
1170
|
+
} else if (violation.errorThreshold) {
|
|
1171
|
+
parts.push(`threshold ${sanitizeText(violation.errorThreshold)}`);
|
|
1172
|
+
}
|
|
1173
|
+
return parts.join(" | ");
|
|
1174
|
+
}
|
|
1175
|
+
function formatLocation(path, line) {
|
|
1176
|
+
if (!path) return void 0;
|
|
1177
|
+
const safePath = sanitizeText(path);
|
|
1178
|
+
if (!line) return safePath;
|
|
1179
|
+
return `${safePath}:${String(line)}`;
|
|
1180
|
+
}
|
|
1181
|
+
function formatRuleWithName(rule, ruleName) {
|
|
1182
|
+
const safeRule = sanitizeText(rule);
|
|
1183
|
+
if (!ruleName) return safeRule;
|
|
1184
|
+
return `${safeRule} (${sanitizeText(ruleName)})`;
|
|
1185
|
+
}
|
|
1186
|
+
function formatSonarIssue(issue) {
|
|
1187
|
+
const lines = [` - ${sanitizeText(issue.message)}`];
|
|
1188
|
+
const location = formatLocation(issue.path, issue.line);
|
|
1189
|
+
if (location) lines.push(` Location: ${location}`);
|
|
1190
|
+
if (issue.severity || issue.type) {
|
|
1191
|
+
const labels = [issue.severity, issue.type].filter((label) => Boolean(label)).map(sanitizeText).join(" / ");
|
|
1192
|
+
lines.push(` Classification: ${labels}`);
|
|
1193
|
+
}
|
|
1194
|
+
if (issue.rule) lines.push(` Rule: ${sanitizeText(issue.rule)}`);
|
|
1195
|
+
if (issue.explanation) lines.push(` Explanation: ${sanitizeText(issue.explanation)}`);
|
|
1196
|
+
return lines;
|
|
1197
|
+
}
|
|
1198
|
+
function formatSonarHotspot(hotspot) {
|
|
1199
|
+
const lines = [` - ${sanitizeText(hotspot.message)}`];
|
|
1200
|
+
const location = formatLocation(hotspot.path, hotspot.line);
|
|
1201
|
+
if (location) lines.push(` Location: ${location}`);
|
|
1202
|
+
if (hotspot.status) lines.push(` Status: ${sanitizeText(hotspot.status)}`);
|
|
1203
|
+
if (hotspot.vulnerabilityProbability || hotspot.securityCategory) {
|
|
1204
|
+
const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((label) => Boolean(label)).map(sanitizeText).join(" / ");
|
|
1205
|
+
lines.push(` Classification: ${labels}`);
|
|
1206
|
+
}
|
|
1207
|
+
if (hotspot.rule) {
|
|
1208
|
+
lines.push(` Rule: ${formatRuleWithName(hotspot.rule, hotspot.ruleName)}`);
|
|
1209
|
+
}
|
|
1210
|
+
if (hotspot.riskDescription) lines.push(` Risk: ${sanitizeText(hotspot.riskDescription)}`);
|
|
1211
|
+
if (hotspot.vulnerabilityDescription) lines.push(` Review: ${sanitizeText(hotspot.vulnerabilityDescription)}`);
|
|
1212
|
+
if (hotspot.fixRecommendations) lines.push(` Fix: ${sanitizeText(hotspot.fixRecommendations)}`);
|
|
1213
|
+
return lines;
|
|
1214
|
+
}
|
|
1215
|
+
function appendGateViolations(lines, gateViolations) {
|
|
1216
|
+
if (gateViolations.length === 0) return;
|
|
1217
|
+
lines.push(" Gate Violations:");
|
|
1218
|
+
for (const violation of gateViolations) {
|
|
1219
|
+
lines.push(` - ${formatGateViolation(violation)}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function appendSonarIssues(lines, issues) {
|
|
1223
|
+
if (issues.length === 0) return;
|
|
1224
|
+
lines.push(" Issues:");
|
|
1225
|
+
for (const issue of issues) {
|
|
1226
|
+
lines.push(...formatSonarIssue(issue));
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function appendSecurityHotspots(lines, securityHotspots) {
|
|
1230
|
+
if (securityHotspots.length === 0) return;
|
|
1231
|
+
lines.push(" Security Hotspots:");
|
|
1232
|
+
for (const hotspot of securityHotspots) {
|
|
1233
|
+
lines.push(...formatSonarHotspot(hotspot));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function hasSonarFailureDetails(sonarFailures) {
|
|
1237
|
+
return Boolean(sonarFailures.qualityGateStatus) || sonarFailures.gateViolations.length > 0 || sonarFailures.issues.length > 0 || sonarFailures.securityHotspots.length > 0;
|
|
1238
|
+
}
|
|
1239
|
+
function formatSonarFailures(sonarFailures, sonarcloudUrl) {
|
|
1240
|
+
const lines = ["Sonar Failures:"];
|
|
1241
|
+
if (sonarFailures.status === "private") {
|
|
1242
|
+
lines.push(` Note: ${sanitizeText(sonarFailures.privateMessage ?? "SonarCloud project is private.")}`);
|
|
1243
|
+
return lines.join("\n") + "\n";
|
|
1244
|
+
}
|
|
1245
|
+
if (sonarFailures.status === "unavailable") {
|
|
1246
|
+
lines.push(` Note: ${sanitizeText(sonarFailures.unavailableMessage ?? "SonarCloud failure details are unavailable.")}`);
|
|
1247
|
+
if (sonarcloudUrl) lines.push(` URL: ${sonarcloudUrl}`);
|
|
1248
|
+
return lines.join("\n") + "\n";
|
|
1249
|
+
}
|
|
1250
|
+
if (sonarFailures.qualityGateStatus) {
|
|
1251
|
+
lines.push(` Quality Gate: ${sanitizeText(sonarFailures.qualityGateStatus)}`);
|
|
1252
|
+
}
|
|
1253
|
+
appendGateViolations(lines, sonarFailures.gateViolations);
|
|
1254
|
+
appendSonarIssues(lines, sonarFailures.issues);
|
|
1255
|
+
appendSecurityHotspots(lines, sonarFailures.securityHotspots);
|
|
1256
|
+
if (!hasSonarFailureDetails(sonarFailures)) {
|
|
1257
|
+
lines.push(" Note: SonarCloud reported a failure but returned no violation, issue, or hotspot details.");
|
|
1258
|
+
if (sonarcloudUrl) lines.push(` URL: ${sonarcloudUrl}`);
|
|
1259
|
+
}
|
|
1260
|
+
return lines.join("\n") + "\n";
|
|
1261
|
+
}
|
|
1262
|
+
function sonarFailureNote(sonarFailures) {
|
|
1263
|
+
if (!sonarFailures) return void 0;
|
|
1264
|
+
if (sonarFailures.status === "private") return sonarFailures.privateMessage;
|
|
1265
|
+
if (sonarFailures.status === "unavailable") return sonarFailures.unavailableMessage;
|
|
1266
|
+
return void 0;
|
|
1267
|
+
}
|
|
792
1268
|
var POLL_INTERVAL_MS = 1e4;
|
|
793
1269
|
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
1270
|
"after",
|
|
@@ -858,12 +1334,20 @@ URL: ${pr.url}
|
|
|
858
1334
|
const issueStr = pr.sonarNewIssues === null || pr.sonarNewIssues === void 0 ? "unavailable" : String(pr.sonarNewIssues);
|
|
859
1335
|
process.stdout.write(`Sonar New Issues: ${issueStr}
|
|
860
1336
|
`);
|
|
1337
|
+
const failureNote = sonarFailureNote(pr.sonarFailures);
|
|
1338
|
+
if (pr.sonarNewIssuesNote && pr.sonarNewIssuesNote !== failureNote) {
|
|
1339
|
+
process.stdout.write(`Sonar Note: ${sanitizeText(pr.sonarNewIssuesNote)}
|
|
1340
|
+
`);
|
|
1341
|
+
}
|
|
861
1342
|
}
|
|
862
1343
|
process.stdout.write(formatCheckSummary(pr.checks));
|
|
863
1344
|
process.stdout.write(formatChecks(pr.checks));
|
|
864
1345
|
if (failed.length > 0) {
|
|
865
1346
|
process.stdout.write(formatFailedChecks(failed));
|
|
866
1347
|
}
|
|
1348
|
+
if (pr.sonarFailures !== void 0) {
|
|
1349
|
+
process.stdout.write(formatSonarFailures(pr.sonarFailures, pr.sonarcloudUrl));
|
|
1350
|
+
}
|
|
867
1351
|
}
|
|
868
1352
|
});
|
|
869
1353
|
var ANSI_ESCAPE_RE = new RegExp("\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "g");
|
|
@@ -1075,6 +1559,7 @@ minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
|
|
|
1075
1559
|
var gitCommand = new Command2("git").description("Git workflow commands (some require gh CLI)").addCommand(getPrInfoCmd).addCommand(getPrCommentsCmd).addCommand(finishFeatureCmd).addCommand(publishReleaseCmd);
|
|
1076
1560
|
|
|
1077
1561
|
// src/commands/getReady.ts
|
|
1562
|
+
import { createInterface as createInterface2 } from "readline";
|
|
1078
1563
|
import { Command as Command3 } from "commander";
|
|
1079
1564
|
|
|
1080
1565
|
// src/config/githubService.ts
|
|
@@ -1094,14 +1579,14 @@ function run3(cmd, args) {
|
|
|
1094
1579
|
status: result.status ?? 1
|
|
1095
1580
|
};
|
|
1096
1581
|
}
|
|
1097
|
-
function listIssues(technique, value) {
|
|
1582
|
+
function listIssues(technique, value, limit = 10) {
|
|
1098
1583
|
const baseArgs = [
|
|
1099
1584
|
"issue",
|
|
1100
1585
|
"list",
|
|
1101
1586
|
"--state",
|
|
1102
1587
|
"open",
|
|
1103
1588
|
"--limit",
|
|
1104
|
-
|
|
1589
|
+
String(limit),
|
|
1105
1590
|
"--json",
|
|
1106
1591
|
"number,title,body,url"
|
|
1107
1592
|
];
|
|
@@ -1121,11 +1606,7 @@ function listIssues(technique, value) {
|
|
|
1121
1606
|
if (status !== 0) {
|
|
1122
1607
|
throw new Error(stderr.trim() || "Failed to query GitHub issues. Is `gh` installed and authenticated?");
|
|
1123
1608
|
}
|
|
1124
|
-
|
|
1125
|
-
if (issues.length === 0) {
|
|
1126
|
-
return null;
|
|
1127
|
-
}
|
|
1128
|
-
return issues[0];
|
|
1609
|
+
return JSON.parse(stdout);
|
|
1129
1610
|
}
|
|
1130
1611
|
function postComment(issueNumber, body) {
|
|
1131
1612
|
const { stderr, status } = run3("gh", ["issue", "comment", String(issueNumber), "--body", body]);
|
|
@@ -1138,7 +1619,7 @@ function postComment(issueNumber, body) {
|
|
|
1138
1619
|
import { spawn, spawnSync as spawnSync4 } from "child_process";
|
|
1139
1620
|
import { createInterface } from "readline";
|
|
1140
1621
|
import { existsSync } from "fs";
|
|
1141
|
-
import { delimiter, join as
|
|
1622
|
+
import { delimiter, join as join3 } from "path";
|
|
1142
1623
|
|
|
1143
1624
|
// src/cli/spawnUtils.ts
|
|
1144
1625
|
function truncate(str, max) {
|
|
@@ -1173,7 +1654,7 @@ function handleExitCode(status, toolName) {
|
|
|
1173
1654
|
function resolveCommand(name) {
|
|
1174
1655
|
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
1175
1656
|
for (const dir of pathDirs) {
|
|
1176
|
-
const candidate =
|
|
1657
|
+
const candidate = join3(dir, name);
|
|
1177
1658
|
if (existsSync(candidate)) return candidate;
|
|
1178
1659
|
}
|
|
1179
1660
|
return name;
|
|
@@ -1209,7 +1690,7 @@ function invokeClaudeCodeSync(prompt, yolo, model) {
|
|
|
1209
1690
|
handleExitCode(result.status, "Claude Code");
|
|
1210
1691
|
}
|
|
1211
1692
|
function invokeClaudeCodeVerbose(prompt, yolo, model) {
|
|
1212
|
-
return new Promise((
|
|
1693
|
+
return new Promise((resolve3) => {
|
|
1213
1694
|
const claudeBin = resolveCommand("claude");
|
|
1214
1695
|
const args = [];
|
|
1215
1696
|
if (yolo) args.push("--dangerously-skip-permissions");
|
|
@@ -1231,7 +1712,7 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
|
|
|
1231
1712
|
});
|
|
1232
1713
|
child.on("close", (code) => {
|
|
1233
1714
|
handleExitCode(code, "Claude Code");
|
|
1234
|
-
|
|
1715
|
+
resolve3();
|
|
1235
1716
|
});
|
|
1236
1717
|
});
|
|
1237
1718
|
}
|
|
@@ -1305,12 +1786,13 @@ function invokeCodexCode(prompt, options = {}) {
|
|
|
1305
1786
|
if (options.verbose) {
|
|
1306
1787
|
process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
|
|
1307
1788
|
}
|
|
1308
|
-
invokeCodexCodeSync(prompt, options.yolo ?? false);
|
|
1789
|
+
invokeCodexCodeSync(prompt, options.yolo ?? false, options.model);
|
|
1309
1790
|
}
|
|
1310
|
-
function invokeCodexCodeSync(prompt, yolo) {
|
|
1791
|
+
function invokeCodexCodeSync(prompt, yolo, model) {
|
|
1311
1792
|
const codexBin = resolveCommand("codex");
|
|
1312
1793
|
const args = ["exec"];
|
|
1313
1794
|
if (yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
1795
|
+
if (model) args.push("--model", model);
|
|
1314
1796
|
args.push(prompt);
|
|
1315
1797
|
const result = spawnSync5(codexBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
1316
1798
|
handleSpawnError(result.error, "codex");
|
|
@@ -1318,8 +1800,37 @@ function invokeCodexCodeSync(prompt, yolo) {
|
|
|
1318
1800
|
}
|
|
1319
1801
|
|
|
1320
1802
|
// src/commands/getReady.ts
|
|
1321
|
-
|
|
1322
|
-
|
|
1803
|
+
function writeOverflowHint(output, issues, limit) {
|
|
1804
|
+
if (issues.length === limit) {
|
|
1805
|
+
output.write(`(Showing first ${limit} matching issues \u2014 there may be more. Use --limit to fetch more.)
|
|
1806
|
+
`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function writeIssueList(output, issues, limit) {
|
|
1810
|
+
output.write("\nAvailable issues:\n");
|
|
1811
|
+
for (let i = 0; i < issues.length; i++) {
|
|
1812
|
+
output.write(` [${i + 1}] #${issues[i].number} - ${issues[i].title}
|
|
1813
|
+
`);
|
|
1814
|
+
}
|
|
1815
|
+
writeOverflowHint(output, issues, limit);
|
|
1816
|
+
}
|
|
1817
|
+
async function promptSelection(issues, limit, output) {
|
|
1818
|
+
writeIssueList(output, issues, limit);
|
|
1819
|
+
const rl = createInterface2({ input: process.stdin, output });
|
|
1820
|
+
const answer = await new Promise(
|
|
1821
|
+
(resolve3) => rl.question(`
|
|
1822
|
+
Select issue (1-${issues.length}): `, resolve3)
|
|
1823
|
+
);
|
|
1824
|
+
rl.close();
|
|
1825
|
+
const n = Number.parseInt(answer.trim(), 10);
|
|
1826
|
+
if (Number.isNaN(n) || n < 1 || n > issues.length) {
|
|
1827
|
+
process.stderr.write(`Error: Invalid selection "${answer.trim()}". Enter a number between 1 and ${issues.length}.
|
|
1828
|
+
`);
|
|
1829
|
+
process.exit(1);
|
|
1830
|
+
}
|
|
1831
|
+
return issues[n - 1];
|
|
1832
|
+
}
|
|
1833
|
+
function validateConfig(config) {
|
|
1323
1834
|
if (config.remoteType !== "gh") {
|
|
1324
1835
|
process.stderr.write(
|
|
1325
1836
|
"Error: implement-next is not supported in Azure DevOps mode. Work item discovery is not available in azdo-cli. See docs/azdo-gap.md for details.\n"
|
|
@@ -1338,24 +1849,58 @@ var implementNextCommand = new Command3("implement-next").description("Find the
|
|
|
1338
1849
|
);
|
|
1339
1850
|
process.exit(1);
|
|
1340
1851
|
}
|
|
1852
|
+
}
|
|
1853
|
+
async function resolveIssue(issues, options, limit) {
|
|
1854
|
+
const selectionOutput = options.json ? process.stderr : process.stdout;
|
|
1855
|
+
if (issues.length === 0) {
|
|
1856
|
+
process.stdout.write("No issues found matching the configured filter.\n");
|
|
1857
|
+
process.exit(0);
|
|
1858
|
+
}
|
|
1859
|
+
if (issues.length > 1 && options.queryOnly) {
|
|
1860
|
+
writeIssueList(selectionOutput, issues, limit);
|
|
1861
|
+
process.exit(0);
|
|
1862
|
+
}
|
|
1341
1863
|
let issue;
|
|
1864
|
+
if (issues.length === 1) {
|
|
1865
|
+
issue = issues[0];
|
|
1866
|
+
selectionOutput.write(`Issue: #${issue.number}
|
|
1867
|
+
Title: ${issue.title}
|
|
1868
|
+
`);
|
|
1869
|
+
} else if (options.takeFirst) {
|
|
1870
|
+
issue = issues[0];
|
|
1871
|
+
selectionOutput.write(`Selecting issue #${issue.number}: ${issue.title}
|
|
1872
|
+
`);
|
|
1873
|
+
} else {
|
|
1874
|
+
issue = await promptSelection(issues, limit, selectionOutput);
|
|
1875
|
+
selectionOutput.write(`
|
|
1876
|
+
Issue: #${issue.number}
|
|
1877
|
+
Title: ${issue.title}
|
|
1878
|
+
`);
|
|
1879
|
+
}
|
|
1880
|
+
return issue;
|
|
1881
|
+
}
|
|
1882
|
+
var implementNextCommand = new Command3("implement-next").description("Find the next open GitHub issue matching the configured filter, claim it, and invoke the AI code assistant (Claude or Codex)").option("--json", "Output issue details as JSON").option("--no-claude", "Skip all AI invocation (Claude or Codex) after claiming the issue").option("--codex", "Use Codex CLI instead of Claude Code").option("--query-only", "Print issue content and exit without claiming or invoking any AI tools").option("--yolo", "Launch with --dangerously-skip-permissions (Claude) or --dangerously-bypass-approvals-and-sandbox (Codex)").option("--verbose", "Show step-by-step progress summary and final result").option("--opus", "Use claude-opus-4-6").option("--sonnet", "Use claude-sonnet-4-6").option("--haiku", "Use claude-haiku-4-5-20251001").option("--take-first", "When multiple issues match, pick the first without prompting").option("--limit <n>", "Max issues to fetch and display (default: 10)", "10").action(async (options) => {
|
|
1883
|
+
const config = readConfig();
|
|
1884
|
+
validateConfig(config);
|
|
1885
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
1886
|
+
if (Number.isNaN(limit) || limit <= 0) {
|
|
1887
|
+
process.stderr.write(`Error: --limit must be a positive integer (got "${options.limit}").
|
|
1888
|
+
`);
|
|
1889
|
+
process.exit(1);
|
|
1890
|
+
}
|
|
1891
|
+
let issues;
|
|
1342
1892
|
try {
|
|
1343
|
-
|
|
1893
|
+
issues = listIssues(config.issueDiscoveryTechnique, config.issueDiscoveryValue, limit);
|
|
1344
1894
|
} catch (err) {
|
|
1345
1895
|
process.stderr.write(`Error: ${err.message}
|
|
1346
1896
|
`);
|
|
1347
1897
|
process.exit(1);
|
|
1348
1898
|
}
|
|
1349
|
-
|
|
1350
|
-
process.stdout.write("No issues found matching the configured filter.\n");
|
|
1351
|
-
process.exit(0);
|
|
1352
|
-
}
|
|
1899
|
+
const issue = await resolveIssue(issues, options, limit);
|
|
1353
1900
|
if (options.json) {
|
|
1354
1901
|
process.stdout.write(JSON.stringify({ number: issue.number, title: issue.title, body: issue.body, url: issue.url }, null, 2) + "\n");
|
|
1355
1902
|
} else {
|
|
1356
|
-
process.stdout.write(`
|
|
1357
|
-
Title: ${issue.title}
|
|
1358
|
-
URL: ${issue.url}
|
|
1903
|
+
process.stdout.write(`URL: ${issue.url}
|
|
1359
1904
|
|
|
1360
1905
|
${issue.body}
|
|
1361
1906
|
`);
|
|
@@ -1372,7 +1917,9 @@ ${issue.body}
|
|
|
1372
1917
|
}
|
|
1373
1918
|
if (options.claude !== false) {
|
|
1374
1919
|
const systemPrompt = config.claudeSystemPrompt ?? DEFAULT_CLAUDE_SYSTEM_PROMPT;
|
|
1375
|
-
const prompt =
|
|
1920
|
+
const prompt = `Resolving issue #${issue.number}:
|
|
1921
|
+
|
|
1922
|
+
${systemPrompt}
|
|
1376
1923
|
|
|
1377
1924
|
${issue.body}`;
|
|
1378
1925
|
if (options.codex) {
|
|
@@ -1384,16 +1931,62 @@ ${issue.body}`;
|
|
|
1384
1931
|
}
|
|
1385
1932
|
});
|
|
1386
1933
|
|
|
1387
|
-
// src/commands/
|
|
1934
|
+
// src/commands/execute.ts
|
|
1935
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1388
1936
|
import { Command as Command4 } from "commander";
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1937
|
+
function readStdin() {
|
|
1938
|
+
return new Promise((resolve3, reject) => {
|
|
1939
|
+
const chunks = [];
|
|
1940
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
1941
|
+
process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
|
|
1942
|
+
process.stdin.on("error", reject);
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
async function resolvePrompt(options) {
|
|
1946
|
+
const hasCli = options.prompt !== void 0;
|
|
1947
|
+
const hasFile = options.filePrompt !== void 0;
|
|
1948
|
+
if (hasCli && hasFile) {
|
|
1949
|
+
process.stderr.write("Error: --prompt is mutually exclusive with --file-prompt.\n");
|
|
1950
|
+
process.exit(1);
|
|
1951
|
+
}
|
|
1952
|
+
if (hasCli) {
|
|
1953
|
+
return options.prompt;
|
|
1954
|
+
}
|
|
1955
|
+
if (hasFile) {
|
|
1956
|
+
const path = options.filePrompt;
|
|
1957
|
+
try {
|
|
1958
|
+
return readFileSync3(path, "utf8");
|
|
1959
|
+
} catch {
|
|
1960
|
+
process.stderr.write(`Error: Cannot read file: ${path}
|
|
1961
|
+
`);
|
|
1962
|
+
process.exit(1);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
if (!process.stdin.isTTY) {
|
|
1966
|
+
const content = await readStdin();
|
|
1967
|
+
if (content.trim().length === 0) {
|
|
1968
|
+
process.stderr.write("Error: No prompt provided. Use --prompt, --file-prompt, or pipe via stdin.\n");
|
|
1969
|
+
process.exit(1);
|
|
1970
|
+
}
|
|
1971
|
+
return content;
|
|
1972
|
+
}
|
|
1973
|
+
process.stderr.write("Error: No prompt provided. Use --prompt, --file-prompt, or pipe via stdin.\n");
|
|
1974
|
+
process.exit(1);
|
|
1975
|
+
}
|
|
1976
|
+
var executeCommand = new Command4("execute").description("Delegate work to an AI executor").requiredOption("--with <executor>", "Executor to use: claude or codex").option("--prompt <string>", "Prompt to send to the executor").option("--file-prompt <path>", "Path to a file whose content is used as the prompt").option("--silent", "Suppress step-by-step Claude output; show only the final summary").option("--model <string>", "Model identifier to pass to the executor").action(async (options) => {
|
|
1977
|
+
const executor = options.with.toLowerCase();
|
|
1978
|
+
if (executor !== "claude" && executor !== "codex") {
|
|
1979
|
+
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${options.with}'.
|
|
1980
|
+
`);
|
|
1981
|
+
process.exit(1);
|
|
1982
|
+
}
|
|
1983
|
+
const prompt = await resolvePrompt(options);
|
|
1984
|
+
if (executor === "codex") {
|
|
1985
|
+
invokeCodexCode(prompt, { yolo: true, model: options.model });
|
|
1986
|
+
} else {
|
|
1987
|
+
await invokeClaudeCode(prompt, { yolo: true, verbose: !options.silent, model: options.model });
|
|
1988
|
+
}
|
|
1395
1989
|
});
|
|
1396
|
-
var testCommand = new Command4("test").description("Test commands for verifying automata integrations").addCommand(testClaudeCmd).addCommand(testCodexCmd);
|
|
1397
1990
|
|
|
1398
1991
|
// src/commands/executePrompt.ts
|
|
1399
1992
|
import { Command as Command5 } from "commander";
|
|
@@ -1403,6 +1996,9 @@ function withPush(prompt, push) {
|
|
|
1403
1996
|
|
|
1404
1997
|
${PUSH_INSTRUCTION}` : prompt;
|
|
1405
1998
|
}
|
|
1999
|
+
function formatPrInfoContext(pr) {
|
|
2000
|
+
return JSON.stringify(pr, null, 2);
|
|
2001
|
+
}
|
|
1406
2002
|
function addAiOptions(cmd) {
|
|
1407
2003
|
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
2004
|
}
|
|
@@ -1444,7 +2040,10 @@ var executeSonarCmd = addAiOptions(
|
|
|
1444
2040
|
const fullPrompt = withPush(
|
|
1445
2041
|
`${sonarPromptText}
|
|
1446
2042
|
|
|
1447
|
-
SonarCloud analysis URL: ${pr.sonarcloudUrl}
|
|
2043
|
+
SonarCloud analysis URL: ${pr.sonarcloudUrl}
|
|
2044
|
+
|
|
2045
|
+
Current PR context from automata git get-pr-info --json:
|
|
2046
|
+
${formatPrInfoContext(pr)}`,
|
|
1448
2047
|
options.push
|
|
1449
2048
|
);
|
|
1450
2049
|
if (options.codex) {
|
|
@@ -1517,7 +2116,7 @@ program.name("automata").description("Automata CLI tool").version(version, "-v,
|
|
|
1517
2116
|
program.addCommand(configCommand);
|
|
1518
2117
|
program.addCommand(gitCommand);
|
|
1519
2118
|
program.addCommand(implementNextCommand);
|
|
1520
|
-
program.addCommand(
|
|
2119
|
+
program.addCommand(executeCommand);
|
|
1521
2120
|
program.addCommand(executePromptCommand);
|
|
1522
2121
|
program.showHelpAfterError();
|
|
1523
2122
|
program.parse();
|