@g-abhishek/gitx 0.1.2 → 0.1.5
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 +386 -3
- package/dist/ai/claudeAi.d.ts +35 -0
- package/dist/ai/claudeAi.d.ts.map +1 -0
- package/dist/ai/claudeAi.js +396 -0
- package/dist/ai/claudeAi.js.map +1 -0
- package/dist/ai/claudeCliAi.d.ts +27 -0
- package/dist/ai/claudeCliAi.d.ts.map +1 -0
- package/dist/ai/claudeCliAi.js +312 -0
- package/dist/ai/claudeCliAi.js.map +1 -0
- package/dist/ai/localClaudeAi.d.ts +2 -0
- package/dist/ai/localClaudeAi.d.ts.map +1 -0
- package/dist/ai/localClaudeAi.js +4 -0
- package/dist/ai/localClaudeAi.js.map +1 -0
- package/dist/ai/mockAi.d.ts +8 -1
- package/dist/ai/mockAi.d.ts.map +1 -1
- package/dist/ai/mockAi.js +57 -0
- package/dist/ai/mockAi.js.map +1 -1
- package/dist/ai/openAiAi.d.ts +33 -0
- package/dist/ai/openAiAi.d.ts.map +1 -0
- package/dist/ai/openAiAi.js +388 -0
- package/dist/ai/openAiAi.js.map +1 -0
- package/dist/ai/reviewHelpers.d.ts +66 -0
- package/dist/ai/reviewHelpers.d.ts.map +1 -0
- package/dist/ai/reviewHelpers.js +574 -0
- package/dist/ai/reviewHelpers.js.map +1 -0
- package/dist/ai/types.d.ts +247 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/ai/types.js.map +1 -1
- package/dist/cli/commands/ask.d.ts +27 -0
- package/dist/cli/commands/ask.d.ts.map +1 -0
- package/dist/cli/commands/ask.js +230 -0
- package/dist/cli/commands/ask.js.map +1 -0
- package/dist/cli/commands/commit.d.ts +16 -0
- package/dist/cli/commands/commit.d.ts.map +1 -0
- package/dist/cli/commands/commit.js +163 -0
- package/dist/cli/commands/commit.js.map +1 -0
- package/dist/cli/commands/config.d.ts +4 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +666 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/implement.d.ts.map +1 -1
- package/dist/cli/commands/implement.js +149 -31
- package/dist/cli/commands/implement.js.map +1 -1
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +7 -69
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/port.d.ts +32 -0
- package/dist/cli/commands/port.d.ts.map +1 -0
- package/dist/cli/commands/port.js +554 -0
- package/dist/cli/commands/port.js.map +1 -0
- package/dist/cli/commands/pr/close.d.ts +15 -0
- package/dist/cli/commands/pr/close.d.ts.map +1 -0
- package/dist/cli/commands/pr/close.js +71 -0
- package/dist/cli/commands/pr/close.js.map +1 -0
- package/dist/cli/commands/pr/create.d.ts +17 -0
- package/dist/cli/commands/pr/create.d.ts.map +1 -1
- package/dist/cli/commands/pr/create.js +208 -7
- package/dist/cli/commands/pr/create.js.map +1 -1
- package/dist/cli/commands/pr/fixComments.d.ts +5 -2
- package/dist/cli/commands/pr/fixComments.d.ts.map +1 -1
- package/dist/cli/commands/pr/fixComments.js +5 -13
- package/dist/cli/commands/pr/fixComments.js.map +1 -1
- package/dist/cli/commands/pr/index.d.ts.map +1 -1
- package/dist/cli/commands/pr/index.js +6 -2
- package/dist/cli/commands/pr/index.js.map +1 -1
- package/dist/cli/commands/pr/list.d.ts.map +1 -1
- package/dist/cli/commands/pr/list.js +24 -4
- package/dist/cli/commands/pr/list.js.map +1 -1
- package/dist/cli/commands/pr/merge.d.ts +23 -0
- package/dist/cli/commands/pr/merge.d.ts.map +1 -0
- package/dist/cli/commands/pr/merge.js +191 -0
- package/dist/cli/commands/pr/merge.js.map +1 -0
- package/dist/cli/commands/pr/resolve.d.ts +3 -0
- package/dist/cli/commands/pr/resolve.d.ts.map +1 -0
- package/dist/cli/commands/pr/resolve.js +92 -0
- package/dist/cli/commands/pr/resolve.js.map +1 -0
- package/dist/cli/commands/pr/review.d.ts.map +1 -1
- package/dist/cli/commands/pr/review.js +121 -6
- package/dist/cli/commands/pr/review.js.map +1 -1
- package/dist/cli/commands/push.d.ts +16 -0
- package/dist/cli/commands/push.d.ts.map +1 -0
- package/dist/cli/commands/push.js +166 -0
- package/dist/cli/commands/push.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +24 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +414 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +34 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config.d.ts +20 -3
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +98 -45
- package/dist/config/config.js.map +1 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +61 -6
- package/dist/config/schema.js.map +1 -1
- package/dist/core/context.d.ts +6 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js.map +1 -1
- package/dist/core/gitx.d.ts +43 -0
- package/dist/core/gitx.d.ts.map +1 -1
- package/dist/core/gitx.js +187 -20
- package/dist/core/gitx.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/azure.d.ts +26 -0
- package/dist/providers/azure.d.ts.map +1 -0
- package/dist/providers/azure.js +256 -0
- package/dist/providers/azure.js.map +1 -0
- package/dist/providers/base.d.ts +104 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +5 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/factory.d.ts +8 -0
- package/dist/providers/factory.d.ts.map +1 -0
- package/dist/providers/factory.js +25 -0
- package/dist/providers/factory.js.map +1 -0
- package/dist/providers/github.d.ts +19 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +291 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/gitlab.d.ts +19 -0
- package/dist/providers/gitlab.d.ts.map +1 -0
- package/dist/providers/gitlab.js +186 -0
- package/dist/providers/gitlab.js.map +1 -0
- package/dist/types/config.d.ts +50 -7
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/utils/azureAuth.d.ts +51 -0
- package/dist/utils/azureAuth.d.ts.map +1 -0
- package/dist/utils/azureAuth.js +172 -0
- package/dist/utils/azureAuth.js.map +1 -0
- package/dist/utils/git.d.ts +19 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +45 -8
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/gitOps.d.ts +125 -0
- package/dist/utils/gitOps.d.ts.map +1 -0
- package/dist/utils/gitOps.js +396 -0
- package/dist/utils/gitOps.js.map +1 -0
- package/dist/utils/lockFile.d.ts +13 -0
- package/dist/utils/lockFile.d.ts.map +1 -0
- package/dist/utils/lockFile.js +54 -0
- package/dist/utils/lockFile.js.map +1 -0
- package/dist/utils/retry.d.ts +10 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +31 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/workflows/implement.d.ts +41 -0
- package/dist/workflows/implement.d.ts.map +1 -0
- package/dist/workflows/implement.js +219 -0
- package/dist/workflows/implement.js.map +1 -0
- package/dist/workflows/pr.d.ts +41 -0
- package/dist/workflows/pr.d.ts.map +1 -0
- package/dist/workflows/pr.js +291 -0
- package/dist/workflows/pr.js.map +1 -0
- package/dist/workflows/prAddress.d.ts +55 -0
- package/dist/workflows/prAddress.d.ts.map +1 -0
- package/dist/workflows/prAddress.js +349 -0
- package/dist/workflows/prAddress.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR Address Workflow
|
|
3
|
+
*
|
|
4
|
+
* Reads unresolved review comments on a PR, generates AI fixes for each one,
|
|
5
|
+
* applies them to local files, and optionally commits, pushes, and replies
|
|
6
|
+
* to each thread marking it as addressed.
|
|
7
|
+
*
|
|
8
|
+
* Used by:
|
|
9
|
+
* - `gitx pr review <n>` — offered interactively after the review output
|
|
10
|
+
* - `gitx sync` — offered when unresolved comments are detected
|
|
11
|
+
*/
|
|
12
|
+
import ora from "ora";
|
|
13
|
+
import { select, confirm } from "@inquirer/prompts";
|
|
14
|
+
import { writeFile } from "node:fs/promises";
|
|
15
|
+
import { resolve as resolvePath } from "node:path";
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
17
|
+
import { promisify } from "node:util";
|
|
18
|
+
import { createProvider } from "../providers/factory.js";
|
|
19
|
+
import { readRepoFile } from "../utils/gitOps.js";
|
|
20
|
+
import { logger } from "../logger/logger.js";
|
|
21
|
+
const execFileAsync = promisify(execFile);
|
|
22
|
+
async function git(args, cwd) {
|
|
23
|
+
try {
|
|
24
|
+
const result = await execFileAsync("git", args, { cwd });
|
|
25
|
+
return { stdout: result.stdout.trim(), stderr: "" };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const e = err;
|
|
29
|
+
return {
|
|
30
|
+
stdout: e.stdout?.trim() ?? "",
|
|
31
|
+
stderr: (e.stderr ?? e.message ?? String(err)).trim(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ─── Patch application ────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Apply a line-range replacement to a file on disk.
|
|
38
|
+
* Replaces lines startLine–endLine (1-based, inclusive) with `replacement`.
|
|
39
|
+
* Returns the new file content, or throws if the line range is invalid.
|
|
40
|
+
*/
|
|
41
|
+
function applyLineReplacement(originalContent, startLine, endLine, replacement) {
|
|
42
|
+
const lines = originalContent.split("\n");
|
|
43
|
+
const total = lines.length;
|
|
44
|
+
if (startLine < 1 || endLine < startLine || endLine > total + 1) {
|
|
45
|
+
throw new Error(`Line range ${startLine}–${endLine} is out of bounds (file has ${total} lines)`);
|
|
46
|
+
}
|
|
47
|
+
const before = lines.slice(0, startLine - 1);
|
|
48
|
+
const after = lines.slice(endLine); // everything after endLine
|
|
49
|
+
const newLines = replacement === "" ? [] : replacement.split("\n");
|
|
50
|
+
return [...before, ...newLines, ...after].join("\n");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Render a human-readable before/after diff for the terminal.
|
|
54
|
+
*/
|
|
55
|
+
function renderLineDiff(filePath, content, startLine, endLine, replacement) {
|
|
56
|
+
const lines = content.split("\n");
|
|
57
|
+
const removed = lines.slice(startLine - 1, endLine);
|
|
58
|
+
const added = replacement === "" ? [] : replacement.split("\n");
|
|
59
|
+
const out = [`\n 📄 ${filePath} (lines ${startLine}–${endLine})\n`];
|
|
60
|
+
for (const l of removed)
|
|
61
|
+
out.push(` \x1b[31m- ${l}\x1b[0m`);
|
|
62
|
+
for (const l of added)
|
|
63
|
+
out.push(` \x1b[32m+ ${l}\x1b[0m`);
|
|
64
|
+
return out.join("\n");
|
|
65
|
+
}
|
|
66
|
+
// ─── Comment filtering ────────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Given the full flat list of PR comments, return only the root inline comments
|
|
69
|
+
* that are genuinely unresolved — i.e.:
|
|
70
|
+
*
|
|
71
|
+
* 1. They are inline (have path + line > 0).
|
|
72
|
+
* 2. They are root comments, not replies (no inReplyToId).
|
|
73
|
+
* 3. None of their replies contain "✅ Addressed" — our bot's resolution marker.
|
|
74
|
+
*
|
|
75
|
+
* This is the single source of truth used by both the address workflow and the
|
|
76
|
+
* sync pre-check. Replacing the old body-prefix heuristic which was fragile and
|
|
77
|
+
* incorrectly filtered out valid comments.
|
|
78
|
+
*/
|
|
79
|
+
export function filterUnresolvedInlineComments(all) {
|
|
80
|
+
// Collect IDs of root comments that already have an "✅ Addressed" reply
|
|
81
|
+
const addressedIds = new Set();
|
|
82
|
+
for (const c of all) {
|
|
83
|
+
if (c.inReplyToId && c.body.trimStart().startsWith("✅ Addressed")) {
|
|
84
|
+
addressedIds.add(c.inReplyToId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return all.filter((c) =>
|
|
88
|
+
// Must be inline (file + line present)
|
|
89
|
+
!!c.path &&
|
|
90
|
+
typeof c.line === "number" &&
|
|
91
|
+
c.line > 0 &&
|
|
92
|
+
// Must be a root comment, not a reply
|
|
93
|
+
!c.inReplyToId &&
|
|
94
|
+
// Must not already be addressed by our bot
|
|
95
|
+
!addressedIds.has(c.id));
|
|
96
|
+
}
|
|
97
|
+
export async function runAddressWorkflow(gitx, prNumber, opts = {}) {
|
|
98
|
+
const mode = opts.mode ?? "interactive";
|
|
99
|
+
const cwd = gitx.cwd;
|
|
100
|
+
const ctx = await gitx.getRepoContext();
|
|
101
|
+
const provider = createProvider(ctx);
|
|
102
|
+
// ── 1. Fetch comments ──────────────────────────────────────────────────────
|
|
103
|
+
const fetchSpinner = ora("Fetching PR review comments…").start();
|
|
104
|
+
const allComments = await provider.getPRComments(ctx.repoSlug, prNumber);
|
|
105
|
+
// Only address root inline comments that haven't been marked "✅ Addressed" yet
|
|
106
|
+
const comments = filterUnresolvedInlineComments(allComments);
|
|
107
|
+
fetchSpinner.succeed(`Found ${comments.length} inline review comment(s) to address` +
|
|
108
|
+
(allComments.length - comments.length > 0
|
|
109
|
+
? ` (${allComments.length - comments.length} general/bot comments skipped)`
|
|
110
|
+
: ""));
|
|
111
|
+
if (comments.length === 0) {
|
|
112
|
+
logger.info("ℹ️ No inline review comments to address.");
|
|
113
|
+
return { addressed: [], filesChanged: [], pushed: false, repliedCount: 0 };
|
|
114
|
+
}
|
|
115
|
+
// ── 2. Load file contents + diff ──────────────────────────────────────────
|
|
116
|
+
const loadSpinner = ora("Loading file context…").start();
|
|
117
|
+
const diff = await provider.getPRDiff(ctx.repoSlug, prNumber).catch(() => "");
|
|
118
|
+
// Build per-file diff sections
|
|
119
|
+
const fileDiffs = new Map();
|
|
120
|
+
let currentFile = "";
|
|
121
|
+
const diffLines = [];
|
|
122
|
+
for (const line of diff.split("\n")) {
|
|
123
|
+
const m = line.match(/^\+\+\+ b\/(.+)/);
|
|
124
|
+
if (m?.[1]) {
|
|
125
|
+
if (currentFile && diffLines.length)
|
|
126
|
+
fileDiffs.set(currentFile, diffLines.join("\n"));
|
|
127
|
+
currentFile = m[1].trim();
|
|
128
|
+
diffLines.length = 0;
|
|
129
|
+
}
|
|
130
|
+
diffLines.push(line);
|
|
131
|
+
}
|
|
132
|
+
if (currentFile && diffLines.length)
|
|
133
|
+
fileDiffs.set(currentFile, diffLines.join("\n"));
|
|
134
|
+
// Build per-file content map (deduplicated — one read per file)
|
|
135
|
+
const fileContents = new Map();
|
|
136
|
+
for (const c of comments) {
|
|
137
|
+
if (!c.path || fileContents.has(c.path))
|
|
138
|
+
continue;
|
|
139
|
+
const content = await readRepoFile(c.path, cwd);
|
|
140
|
+
if (content)
|
|
141
|
+
fileContents.set(c.path, content);
|
|
142
|
+
}
|
|
143
|
+
loadSpinner.succeed(`Loaded context for ${fileContents.size} file(s).`);
|
|
144
|
+
// ── 3. Generate + apply fixes ─────────────────────────────────────────────
|
|
145
|
+
const addressed = [];
|
|
146
|
+
const modifiedFiles = new Map(); // tracks in-memory modified content
|
|
147
|
+
for (let i = 0; i < comments.length; i++) {
|
|
148
|
+
const comment = comments[i];
|
|
149
|
+
const filePath = comment.path;
|
|
150
|
+
const line = comment.line;
|
|
151
|
+
logger.info(`\n${"─".repeat(60)}\n` +
|
|
152
|
+
`📄 [${i + 1}/${comments.length}] ${filePath} · Line ${line}\n` +
|
|
153
|
+
`💬 ${comment.author}: ${comment.body.slice(0, 200)}${comment.body.length > 200 ? "…" : ""}\n`);
|
|
154
|
+
// Use in-memory content if file was already modified in this session
|
|
155
|
+
const currentContent = modifiedFiles.get(filePath) ?? fileContents.get(filePath) ?? "";
|
|
156
|
+
if (!currentContent) {
|
|
157
|
+
logger.warn(` ⚠️ Could not load file content — skipping.`);
|
|
158
|
+
addressed.push({ comment, fix: makeFallbackFix(filePath, line), applied: false, skipped: true, skipReason: "file not found" });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// Generate fix
|
|
162
|
+
const fixSpinner = ora(" 🤖 AI generating fix…").start();
|
|
163
|
+
let fix;
|
|
164
|
+
try {
|
|
165
|
+
fix = await gitx.ai.generateFix({
|
|
166
|
+
comment: comment.body,
|
|
167
|
+
commentAuthor: comment.author,
|
|
168
|
+
filePath,
|
|
169
|
+
line,
|
|
170
|
+
fileContent: currentContent,
|
|
171
|
+
fileDiff: fileDiffs.get(filePath) ?? "",
|
|
172
|
+
});
|
|
173
|
+
fixSpinner.succeed(" AI fix generated.");
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
fixSpinner.fail(` AI fix failed: ${err.message}`);
|
|
177
|
+
addressed.push({ comment, fix: makeFallbackFix(filePath, line), applied: false, skipped: true, skipReason: "AI error" });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Discussion — no code change needed
|
|
181
|
+
if (fix.isDiscussion) {
|
|
182
|
+
logger.info(` 💬 This is a discussion comment — no code change needed.`);
|
|
183
|
+
logger.info(` 📝 ${fix.explanation}`);
|
|
184
|
+
addressed.push({ comment, fix, applied: false, skipped: true, skipReason: "discussion" });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Show the proposed diff
|
|
188
|
+
const diffPreview = renderLineDiff(filePath, currentContent, fix.startLine, fix.endLine, fix.replacement);
|
|
189
|
+
logger.info(diffPreview);
|
|
190
|
+
logger.info(`\n 💡 ${fix.explanation}`);
|
|
191
|
+
logger.info(` Confidence: ${fix.confidence === "high" ? "🟢 High" : "🟡 Low"} · Fully resolves: ${fix.resolves ? "yes" : "partial"}`);
|
|
192
|
+
// Decide whether to apply
|
|
193
|
+
let apply = false;
|
|
194
|
+
if (mode === "auto") {
|
|
195
|
+
apply = fix.confidence === "high";
|
|
196
|
+
if (!apply)
|
|
197
|
+
logger.info(" ⏭️ Low confidence — skipping in auto mode.");
|
|
198
|
+
}
|
|
199
|
+
else if (mode === "no-push" || mode === "interactive") {
|
|
200
|
+
try {
|
|
201
|
+
apply = await confirm({ message: " Apply this fix?", default: fix.confidence === "high" });
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
apply = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!apply) {
|
|
208
|
+
addressed.push({ comment, fix, applied: false, skipped: true, skipReason: "user skipped" });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Apply the patch
|
|
212
|
+
try {
|
|
213
|
+
const newContent = applyLineReplacement(currentContent, fix.startLine, fix.endLine, fix.replacement);
|
|
214
|
+
const absPath = resolvePath(cwd, filePath);
|
|
215
|
+
await writeFile(absPath, newContent, "utf8");
|
|
216
|
+
modifiedFiles.set(filePath, newContent);
|
|
217
|
+
logger.info(` ✅ Applied.`);
|
|
218
|
+
addressed.push({ comment, fix, applied: true, skipped: false });
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
logger.warn(` ❌ Patch failed: ${err.message}`);
|
|
222
|
+
addressed.push({ comment, fix, applied: false, skipped: true, skipReason: `patch error: ${err.message}` });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
logger.info(`\n${"─".repeat(60)}`);
|
|
226
|
+
const appliedCount = addressed.filter((a) => a.applied).length;
|
|
227
|
+
const filesChanged = [...new Set(addressed.filter((a) => a.applied).map((a) => a.fix.file))];
|
|
228
|
+
logger.info(`\n✔ ${appliedCount} fix(es) applied across ${filesChanged.length} file(s).`);
|
|
229
|
+
if (appliedCount === 0) {
|
|
230
|
+
return { addressed, filesChanged: [], pushed: false, repliedCount: 0 };
|
|
231
|
+
}
|
|
232
|
+
// ── 4. Commit + push ──────────────────────────────────────────────────────
|
|
233
|
+
let pushed = false;
|
|
234
|
+
let commitSha;
|
|
235
|
+
const commitMsg = `fix: address PR #${prNumber} review comments (${appliedCount} fix${appliedCount !== 1 ? "es" : ""})`;
|
|
236
|
+
if (mode === "no-push") {
|
|
237
|
+
// Local changes only — no commit, no push
|
|
238
|
+
logger.info("ℹ️ Changes applied locally (--no-push mode). Review and push when ready.");
|
|
239
|
+
}
|
|
240
|
+
else if (mode === "commit-no-push") {
|
|
241
|
+
// Commit the fixes but let the caller (gitx sync) handle the push
|
|
242
|
+
// after it has rebased/merged the branch onto the base.
|
|
243
|
+
const stageSpinner = ora("Staging fix changes…").start();
|
|
244
|
+
await git(["add", ...filesChanged.map((f) => resolvePath(cwd, f))], cwd);
|
|
245
|
+
stageSpinner.succeed("Changes staged.");
|
|
246
|
+
const commitSpinner = ora("Committing fixes…").start();
|
|
247
|
+
const { stderr: commitErr } = await git(["commit", "-m", commitMsg], cwd);
|
|
248
|
+
if (commitErr && commitErr.includes("error")) {
|
|
249
|
+
commitSpinner.fail(`Commit failed: ${commitErr}`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
commitSpinner.succeed(`Committed: "${commitMsg}"`);
|
|
253
|
+
const { stdout: sha } = await git(["rev-parse", "--short", "HEAD"], cwd);
|
|
254
|
+
commitSha = sha;
|
|
255
|
+
}
|
|
256
|
+
logger.info("ℹ️ Fixes committed. Sync will rebase and push everything together.");
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// "interactive" or "auto" — ask user then commit+push
|
|
260
|
+
let shouldPush = false;
|
|
261
|
+
if (mode === "auto") {
|
|
262
|
+
shouldPush = true;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
try {
|
|
266
|
+
const choice = await select({
|
|
267
|
+
message: "What would you like to do with these changes?",
|
|
268
|
+
choices: [
|
|
269
|
+
{ name: "Commit & push — create a commit and push to PR branch", value: "push" },
|
|
270
|
+
{ name: "Keep local only — I'll review the changes first", value: "local" },
|
|
271
|
+
{ name: "Discard all — revert every applied fix", value: "discard" },
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
if (choice === "discard") {
|
|
275
|
+
await git(["checkout", "--", ...filesChanged], cwd);
|
|
276
|
+
logger.info("↩️ All changes discarded.");
|
|
277
|
+
return { addressed, filesChanged: [], pushed: false, repliedCount: 0 };
|
|
278
|
+
}
|
|
279
|
+
shouldPush = choice === "push";
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
shouldPush = false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (shouldPush) {
|
|
286
|
+
const stageSpinner = ora("Staging changes…").start();
|
|
287
|
+
await git(["add", ...filesChanged.map((f) => resolvePath(cwd, f))], cwd);
|
|
288
|
+
stageSpinner.succeed("Changes staged.");
|
|
289
|
+
const commitSpinner = ora("Committing…").start();
|
|
290
|
+
const { stderr: commitErr } = await git(["commit", "-m", commitMsg], cwd);
|
|
291
|
+
if (commitErr && commitErr.includes("error")) {
|
|
292
|
+
commitSpinner.fail(`Commit failed: ${commitErr}`);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
commitSpinner.succeed(`Committed: "${commitMsg}"`);
|
|
296
|
+
const { stdout: sha } = await git(["rev-parse", "--short", "HEAD"], cwd);
|
|
297
|
+
commitSha = sha;
|
|
298
|
+
}
|
|
299
|
+
const pushSpinner = ora("Pushing to remote…").start();
|
|
300
|
+
const { stderr: pushErr } = await git(["push"], cwd);
|
|
301
|
+
if (pushErr && pushErr.includes("error")) {
|
|
302
|
+
pushSpinner.fail(`Push failed: ${pushErr}`);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
pushSpinner.succeed("Pushed ✓");
|
|
306
|
+
pushed = true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
logger.info("ℹ️ Changes kept locally. Run `git push` when ready.");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// ── 5. Reply to addressed comment threads ─────────────────────────────────
|
|
314
|
+
// Only reply after a real push — not in commit-no-push mode (sync will push later
|
|
315
|
+
// and the SHA we have now will change after rebase; reply happens post-push in that flow).
|
|
316
|
+
let repliedCount = 0;
|
|
317
|
+
if (pushed && commitSha) {
|
|
318
|
+
const replySpinner = ora("Replying to addressed comment threads…").start();
|
|
319
|
+
for (const entry of addressed) {
|
|
320
|
+
if (!entry.applied)
|
|
321
|
+
continue;
|
|
322
|
+
const replyBody = `✅ **Addressed** in \`${commitSha}\`\n\n${entry.fix.explanation}` +
|
|
323
|
+
(entry.fix.resolves ? "" : "\n\n> *(Partial fix — please re-review)*");
|
|
324
|
+
try {
|
|
325
|
+
await provider.replyToComment(ctx.repoSlug, prNumber, entry.comment.id, replyBody);
|
|
326
|
+
repliedCount++;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Best-effort — don't abort if a reply fails
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
replySpinner.succeed(`Replied to ${repliedCount} thread(s).`);
|
|
333
|
+
}
|
|
334
|
+
return { addressed, filesChanged, pushed, commitSha, repliedCount };
|
|
335
|
+
}
|
|
336
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
337
|
+
function makeFallbackFix(filePath, line) {
|
|
338
|
+
return {
|
|
339
|
+
file: filePath,
|
|
340
|
+
startLine: line,
|
|
341
|
+
endLine: line,
|
|
342
|
+
replacement: "",
|
|
343
|
+
explanation: "No fix generated.",
|
|
344
|
+
confidence: "low",
|
|
345
|
+
resolves: false,
|
|
346
|
+
isDiscussion: true,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
//# sourceMappingURL=prAddress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prAddress.js","sourceRoot":"","sources":["../../src/workflows/prAddress.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PR Address Workflow\n *\n * Reads unresolved review comments on a PR, generates AI fixes for each one,\n * applies them to local files, and optionally commits, pushes, and replies\n * to each thread marking it as addressed.\n *\n * Used by:\n * - `gitx pr review <n>` — offered interactively after the review output\n * - `gitx sync` — offered when unresolved comments are detected\n */\n\nimport ora from \"ora\";\nimport { select, confirm } from \"@inquirer/prompts\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { resolve as resolvePath } from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { Gitx } from \"../core/gitx.js\";\nimport type { PullRequestComment } from \"../providers/base.js\";\nimport type { AiFixResponse } from \"../ai/types.js\";\nimport { createProvider } from \"../providers/factory.js\";\nimport { readRepoFile } from \"../utils/gitOps.js\";\nimport { logger } from \"../logger/logger.js\";\n\nconst execFileAsync = promisify(execFile);\n\nasync function git(args: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {\n try {\n const result = await execFileAsync(\"git\", args, { cwd });\n return { stdout: result.stdout.trim(), stderr: \"\" };\n } catch (err: unknown) {\n const e = err as { stdout?: string; stderr?: string; message?: string };\n return {\n stdout: e.stdout?.trim() ?? \"\",\n stderr: (e.stderr ?? e.message ?? String(err)).trim(),\n };\n }\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface AddressedComment {\n comment: PullRequestComment;\n fix: AiFixResponse;\n applied: boolean;\n skipped: boolean;\n /** Reason for skipping (user choice, low-confidence rejection, patch error) */\n skipReason?: string;\n}\n\nexport interface AddressWorkflowResult {\n addressed: AddressedComment[];\n filesChanged: string[];\n pushed: boolean;\n commitSha?: string;\n repliedCount: number;\n}\n\n// ─── Patch application ────────────────────────────────────────────────────────\n\n/**\n * Apply a line-range replacement to a file on disk.\n * Replaces lines startLine–endLine (1-based, inclusive) with `replacement`.\n * Returns the new file content, or throws if the line range is invalid.\n */\nfunction applyLineReplacement(\n originalContent: string,\n startLine: number,\n endLine: number,\n replacement: string\n): string {\n const lines = originalContent.split(\"\\n\");\n const total = lines.length;\n\n if (startLine < 1 || endLine < startLine || endLine > total + 1) {\n throw new Error(\n `Line range ${startLine}–${endLine} is out of bounds (file has ${total} lines)`\n );\n }\n\n const before = lines.slice(0, startLine - 1);\n const after = lines.slice(endLine); // everything after endLine\n const newLines = replacement === \"\" ? [] : replacement.split(\"\\n\");\n\n return [...before, ...newLines, ...after].join(\"\\n\");\n}\n\n/**\n * Render a human-readable before/after diff for the terminal.\n */\nfunction renderLineDiff(\n filePath: string,\n content: string,\n startLine: number,\n endLine: number,\n replacement: string\n): string {\n const lines = content.split(\"\\n\");\n const removed = lines.slice(startLine - 1, endLine);\n const added = replacement === \"\" ? [] : replacement.split(\"\\n\");\n\n const out: string[] = [`\\n 📄 ${filePath} (lines ${startLine}–${endLine})\\n`];\n for (const l of removed) out.push(` \\x1b[31m- ${l}\\x1b[0m`);\n for (const l of added) out.push(` \\x1b[32m+ ${l}\\x1b[0m`);\n return out.join(\"\\n\");\n}\n\n// ─── Comment filtering ────────────────────────────────────────────────────────\n\n/**\n * Given the full flat list of PR comments, return only the root inline comments\n * that are genuinely unresolved — i.e.:\n *\n * 1. They are inline (have path + line > 0).\n * 2. They are root comments, not replies (no inReplyToId).\n * 3. None of their replies contain \"✅ Addressed\" — our bot's resolution marker.\n *\n * This is the single source of truth used by both the address workflow and the\n * sync pre-check. Replacing the old body-prefix heuristic which was fragile and\n * incorrectly filtered out valid comments.\n */\nexport function filterUnresolvedInlineComments(\n all: PullRequestComment[]\n): PullRequestComment[] {\n // Collect IDs of root comments that already have an \"✅ Addressed\" reply\n const addressedIds = new Set<number>();\n for (const c of all) {\n if (c.inReplyToId && c.body.trimStart().startsWith(\"✅ Addressed\")) {\n addressedIds.add(c.inReplyToId);\n }\n }\n\n return all.filter(\n (c) =>\n // Must be inline (file + line present)\n !!c.path &&\n typeof c.line === \"number\" &&\n c.line > 0 &&\n // Must be a root comment, not a reply\n !c.inReplyToId &&\n // Must not already be addressed by our bot\n !addressedIds.has(c.id)\n );\n}\n\n// ─── Main workflow ────────────────────────────────────────────────────────────\n\nexport interface AddressWorkflowOptions {\n /**\n * \"interactive\" (default) — show each fix and ask to apply; ask to commit+push at the end.\n * \"auto\" — apply all high-confidence fixes silently, commit and push.\n * \"no-push\" — interactive but never commits or pushes (local changes only).\n * \"commit-no-push\" — interactive, commits the fixes but does NOT push.\n * Used by `gitx sync` so sync can push everything together\n * after rebasing/merging onto the base branch.\n */\n mode?: \"interactive\" | \"auto\" | \"no-push\" | \"commit-no-push\";\n}\n\nexport async function runAddressWorkflow(\n gitx: Gitx,\n prNumber: number,\n opts: AddressWorkflowOptions = {}\n): Promise<AddressWorkflowResult> {\n const mode = opts.mode ?? \"interactive\";\n const cwd = gitx.cwd;\n const ctx = await gitx.getRepoContext();\n const provider = createProvider(ctx);\n\n // ── 1. Fetch comments ──────────────────────────────────────────────────────\n const fetchSpinner = ora(\"Fetching PR review comments…\").start();\n const allComments = await provider.getPRComments(ctx.repoSlug, prNumber);\n // Only address root inline comments that haven't been marked \"✅ Addressed\" yet\n const comments = filterUnresolvedInlineComments(allComments);\n fetchSpinner.succeed(\n `Found ${comments.length} inline review comment(s) to address` +\n (allComments.length - comments.length > 0\n ? ` (${allComments.length - comments.length} general/bot comments skipped)`\n : \"\")\n );\n\n if (comments.length === 0) {\n logger.info(\"ℹ️ No inline review comments to address.\");\n return { addressed: [], filesChanged: [], pushed: false, repliedCount: 0 };\n }\n\n // ── 2. Load file contents + diff ──────────────────────────────────────────\n const loadSpinner = ora(\"Loading file context…\").start();\n const diff = await provider.getPRDiff(ctx.repoSlug, prNumber).catch(() => \"\");\n\n // Build per-file diff sections\n const fileDiffs = new Map<string, string>();\n let currentFile = \"\";\n const diffLines: string[] = [];\n for (const line of diff.split(\"\\n\")) {\n const m = line.match(/^\\+\\+\\+ b\\/(.+)/);\n if (m?.[1]) {\n if (currentFile && diffLines.length) fileDiffs.set(currentFile, diffLines.join(\"\\n\"));\n currentFile = m[1].trim();\n diffLines.length = 0;\n }\n diffLines.push(line);\n }\n if (currentFile && diffLines.length) fileDiffs.set(currentFile, diffLines.join(\"\\n\"));\n\n // Build per-file content map (deduplicated — one read per file)\n const fileContents = new Map<string, string>();\n for (const c of comments) {\n if (!c.path || fileContents.has(c.path)) continue;\n const content = await readRepoFile(c.path, cwd);\n if (content) fileContents.set(c.path, content);\n }\n loadSpinner.succeed(`Loaded context for ${fileContents.size} file(s).`);\n\n // ── 3. Generate + apply fixes ─────────────────────────────────────────────\n const addressed: AddressedComment[] = [];\n const modifiedFiles = new Map<string, string>(); // tracks in-memory modified content\n\n for (let i = 0; i < comments.length; i++) {\n const comment = comments[i]!;\n const filePath = comment.path!;\n const line = comment.line!;\n\n logger.info(\n `\\n${\"─\".repeat(60)}\\n` +\n `📄 [${i + 1}/${comments.length}] ${filePath} · Line ${line}\\n` +\n `💬 ${comment.author}: ${comment.body.slice(0, 200)}${comment.body.length > 200 ? \"…\" : \"\"}\\n`\n );\n\n // Use in-memory content if file was already modified in this session\n const currentContent = modifiedFiles.get(filePath) ?? fileContents.get(filePath) ?? \"\";\n if (!currentContent) {\n logger.warn(` ⚠️ Could not load file content — skipping.`);\n addressed.push({ comment, fix: makeFallbackFix(filePath, line), applied: false, skipped: true, skipReason: \"file not found\" });\n continue;\n }\n\n // Generate fix\n const fixSpinner = ora(\" 🤖 AI generating fix…\").start();\n let fix: AiFixResponse;\n try {\n fix = await gitx.ai.generateFix({\n comment: comment.body,\n commentAuthor: comment.author,\n filePath,\n line,\n fileContent: currentContent,\n fileDiff: fileDiffs.get(filePath) ?? \"\",\n });\n fixSpinner.succeed(\" AI fix generated.\");\n } catch (err) {\n fixSpinner.fail(` AI fix failed: ${(err as Error).message}`);\n addressed.push({ comment, fix: makeFallbackFix(filePath, line), applied: false, skipped: true, skipReason: \"AI error\" });\n continue;\n }\n\n // Discussion — no code change needed\n if (fix.isDiscussion) {\n logger.info(` 💬 This is a discussion comment — no code change needed.`);\n logger.info(` 📝 ${fix.explanation}`);\n addressed.push({ comment, fix, applied: false, skipped: true, skipReason: \"discussion\" });\n continue;\n }\n\n // Show the proposed diff\n const diffPreview = renderLineDiff(filePath, currentContent, fix.startLine, fix.endLine, fix.replacement);\n logger.info(diffPreview);\n logger.info(`\\n 💡 ${fix.explanation}`);\n logger.info(` Confidence: ${fix.confidence === \"high\" ? \"🟢 High\" : \"🟡 Low\"} · Fully resolves: ${fix.resolves ? \"yes\" : \"partial\"}`);\n\n // Decide whether to apply\n let apply = false;\n\n if (mode === \"auto\") {\n apply = fix.confidence === \"high\";\n if (!apply) logger.info(\" ⏭️ Low confidence — skipping in auto mode.\");\n } else if (mode === \"no-push\" || mode === \"interactive\") {\n try {\n apply = await confirm({ message: \" Apply this fix?\", default: fix.confidence === \"high\" });\n } catch {\n apply = false;\n }\n }\n\n if (!apply) {\n addressed.push({ comment, fix, applied: false, skipped: true, skipReason: \"user skipped\" });\n continue;\n }\n\n // Apply the patch\n try {\n const newContent = applyLineReplacement(currentContent, fix.startLine, fix.endLine, fix.replacement);\n const absPath = resolvePath(cwd, filePath);\n await writeFile(absPath, newContent, \"utf8\");\n modifiedFiles.set(filePath, newContent);\n logger.info(` ✅ Applied.`);\n addressed.push({ comment, fix, applied: true, skipped: false });\n } catch (err) {\n logger.warn(` ❌ Patch failed: ${(err as Error).message}`);\n addressed.push({ comment, fix, applied: false, skipped: true, skipReason: `patch error: ${(err as Error).message}` });\n }\n }\n\n logger.info(`\\n${\"─\".repeat(60)}`);\n\n const appliedCount = addressed.filter((a) => a.applied).length;\n const filesChanged = [...new Set(addressed.filter((a) => a.applied).map((a) => a.fix.file))];\n\n logger.info(`\\n✔ ${appliedCount} fix(es) applied across ${filesChanged.length} file(s).`);\n if (appliedCount === 0) {\n return { addressed, filesChanged: [], pushed: false, repliedCount: 0 };\n }\n\n // ── 4. Commit + push ──────────────────────────────────────────────────────\n let pushed = false;\n let commitSha: string | undefined;\n\n const commitMsg = `fix: address PR #${prNumber} review comments (${appliedCount} fix${appliedCount !== 1 ? \"es\" : \"\"})`;\n\n if (mode === \"no-push\") {\n // Local changes only — no commit, no push\n logger.info(\"ℹ️ Changes applied locally (--no-push mode). Review and push when ready.\");\n\n } else if (mode === \"commit-no-push\") {\n // Commit the fixes but let the caller (gitx sync) handle the push\n // after it has rebased/merged the branch onto the base.\n const stageSpinner = ora(\"Staging fix changes…\").start();\n await git([\"add\", ...filesChanged.map((f) => resolvePath(cwd, f))], cwd);\n stageSpinner.succeed(\"Changes staged.\");\n\n const commitSpinner = ora(\"Committing fixes…\").start();\n const { stderr: commitErr } = await git([\"commit\", \"-m\", commitMsg], cwd);\n if (commitErr && commitErr.includes(\"error\")) {\n commitSpinner.fail(`Commit failed: ${commitErr}`);\n } else {\n commitSpinner.succeed(`Committed: \"${commitMsg}\"`);\n const { stdout: sha } = await git([\"rev-parse\", \"--short\", \"HEAD\"], cwd);\n commitSha = sha;\n }\n logger.info(\"ℹ️ Fixes committed. Sync will rebase and push everything together.\");\n\n } else {\n // \"interactive\" or \"auto\" — ask user then commit+push\n let shouldPush = false;\n if (mode === \"auto\") {\n shouldPush = true;\n } else {\n try {\n const choice = await select({\n message: \"What would you like to do with these changes?\",\n choices: [\n { name: \"Commit & push — create a commit and push to PR branch\", value: \"push\" },\n { name: \"Keep local only — I'll review the changes first\", value: \"local\" },\n { name: \"Discard all — revert every applied fix\", value: \"discard\" },\n ],\n });\n if (choice === \"discard\") {\n await git([\"checkout\", \"--\", ...filesChanged], cwd);\n logger.info(\"↩️ All changes discarded.\");\n return { addressed, filesChanged: [], pushed: false, repliedCount: 0 };\n }\n shouldPush = choice === \"push\";\n } catch {\n shouldPush = false;\n }\n }\n\n if (shouldPush) {\n const stageSpinner = ora(\"Staging changes…\").start();\n await git([\"add\", ...filesChanged.map((f) => resolvePath(cwd, f))], cwd);\n stageSpinner.succeed(\"Changes staged.\");\n\n const commitSpinner = ora(\"Committing…\").start();\n const { stderr: commitErr } = await git([\"commit\", \"-m\", commitMsg], cwd);\n if (commitErr && commitErr.includes(\"error\")) {\n commitSpinner.fail(`Commit failed: ${commitErr}`);\n } else {\n commitSpinner.succeed(`Committed: \"${commitMsg}\"`);\n const { stdout: sha } = await git([\"rev-parse\", \"--short\", \"HEAD\"], cwd);\n commitSha = sha;\n }\n\n const pushSpinner = ora(\"Pushing to remote…\").start();\n const { stderr: pushErr } = await git([\"push\"], cwd);\n if (pushErr && pushErr.includes(\"error\")) {\n pushSpinner.fail(`Push failed: ${pushErr}`);\n } else {\n pushSpinner.succeed(\"Pushed ✓\");\n pushed = true;\n }\n } else {\n logger.info(\"ℹ️ Changes kept locally. Run `git push` when ready.\");\n }\n }\n\n // ── 5. Reply to addressed comment threads ─────────────────────────────────\n // Only reply after a real push — not in commit-no-push mode (sync will push later\n // and the SHA we have now will change after rebase; reply happens post-push in that flow).\n let repliedCount = 0;\n if (pushed && commitSha) {\n const replySpinner = ora(\"Replying to addressed comment threads…\").start();\n for (const entry of addressed) {\n if (!entry.applied) continue;\n const replyBody =\n `✅ **Addressed** in \\`${commitSha}\\`\\n\\n${entry.fix.explanation}` +\n (entry.fix.resolves ? \"\" : \"\\n\\n> *(Partial fix — please re-review)*\");\n try {\n await provider.replyToComment(ctx.repoSlug, prNumber, entry.comment.id, replyBody);\n repliedCount++;\n } catch {\n // Best-effort — don't abort if a reply fails\n }\n }\n replySpinner.succeed(`Replied to ${repliedCount} thread(s).`);\n }\n\n return { addressed, filesChanged, pushed, commitSha, repliedCount };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction makeFallbackFix(filePath: string, line: number): AiFixResponse {\n return {\n file: filePath,\n startLine: line,\n endLine: line,\n replacement: \"\",\n explanation: \"No fix generated.\",\n confidence: \"low\",\n resolves: false,\n isDiscussion: true,\n };\n}\n"]}
|