@bobbyg603/mog 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbyg603/mog",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "One command to go from GitHub issue to pull request, powered by Claude Code in a Docker sandbox",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/github.ts CHANGED
@@ -76,6 +76,57 @@ export function listIssues(repo: string, verbose: boolean): void {
76
76
  }
77
77
  }
78
78
 
79
+ export interface PRFeedback {
80
+ prNumber: number;
81
+ prUrl: string;
82
+ reviews: string;
83
+ }
84
+
85
+ export function fetchPRFeedback(repo: string, branchName: string): PRFeedback | null {
86
+ const proc = Bun.spawnSync([
87
+ "gh", "pr", "list",
88
+ "--repo", repo,
89
+ "--head", branchName,
90
+ "--state", "open",
91
+ "--json", "number,url",
92
+ ]);
93
+
94
+ if (proc.exitCode !== 0) return null;
95
+
96
+ const prs = JSON.parse(proc.stdout.toString());
97
+ if (prs.length === 0) return null;
98
+
99
+ const prNumber = prs[0].number;
100
+ const prUrl = prs[0].url;
101
+
102
+ // Fetch review comments
103
+ const reviewProc = Bun.spawnSync([
104
+ "gh", "pr", "view", String(prNumber),
105
+ "--repo", repo,
106
+ "--json", "reviews,comments",
107
+ ]);
108
+
109
+ let reviews = "";
110
+ if (reviewProc.exitCode === 0) {
111
+ const data = JSON.parse(reviewProc.stdout.toString());
112
+
113
+ const reviewEntries = (data.reviews || [])
114
+ .filter((r: { body: string }) => r.body?.trim())
115
+ .map((r: { author: { login: string }; state: string; body: string }) =>
116
+ `**@${r.author.login}** (${r.state}):\n${r.body}`
117
+ );
118
+
119
+ const commentEntries = (data.comments || [])
120
+ .map((c: { author: { login: string }; body: string }) =>
121
+ `**@${c.author.login}:**\n${c.body}`
122
+ );
123
+
124
+ reviews = [...reviewEntries, ...commentEntries].join("\n\n");
125
+ }
126
+
127
+ return { prNumber, prUrl, reviews };
128
+ }
129
+
79
130
  export function pushAndCreatePR(
80
131
  repo: string,
81
132
  worktreeDir: string,
@@ -83,7 +134,8 @@ export function pushAndCreatePR(
83
134
  defaultBranch: string,
84
135
  issueNum: string,
85
136
  issue: Issue,
86
- summary?: string
137
+ summary?: string,
138
+ existingPR?: PRFeedback,
87
139
  ): void {
88
140
  // Check for unpushed commits or uncommitted changes
89
141
  const unpushed = Bun.spawnSync(["git", "log", `origin/${defaultBranch}..HEAD`, "--oneline"], { cwd: worktreeDir });
@@ -129,14 +181,28 @@ export function pushAndCreatePR(
129
181
  }
130
182
  }
131
183
 
132
- // Push
184
+ // Push (force-with-lease when updating an existing PR)
133
185
  log.info(`Pushing branch '${branchName}' to origin...`);
134
- const push = Bun.spawnSync(["git", "push", "-u", "origin", branchName], { cwd: worktreeDir });
186
+ const pushArgs = existingPR
187
+ ? ["git", "push", "--force-with-lease", "-u", "origin", branchName]
188
+ : ["git", "push", "-u", "origin", branchName];
189
+ const push = Bun.spawnSync(pushArgs, { cwd: worktreeDir });
135
190
  if (push.exitCode !== 0) {
136
191
  log.die("Failed to push. Check your git credentials.");
137
192
  }
138
193
  log.ok("Branch pushed.");
139
194
 
195
+ if (existingPR) {
196
+ // Update existing PR
197
+ log.ok("Existing PR updated!");
198
+ console.log(`\x1b[0;32m${existingPR.prUrl}\x1b[0m`);
199
+ console.log();
200
+ log.ok(`All done! Issue #${issueNum} → Branch '${branchName}' → PR updated.`);
201
+ log.info(`Worktree: ${worktreeDir}`);
202
+ log.info(`To clean up the worktree later: git worktree remove ${worktreeDir}`);
203
+ return;
204
+ }
205
+
140
206
  // Create PR
141
207
  log.info("Opening pull request...");
142
208
 
package/src/index.ts CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  import fs from "fs";
4
4
  import path from "path";
5
- import { fetchIssue, listIssues } from "./github";
5
+ import { fetchIssue, listIssues, fetchPRFeedback } from "./github";
6
6
  import { detectRepo, ensureRepo, createWorktree } from "./worktree";
7
7
  import { runClaude } from "./sandbox";
8
8
  import { pushAndCreatePR } from "./github";
9
+ import type { PRFeedback } from "./github";
9
10
  import { log } from "./log";
10
11
 
11
12
  const SANDBOX_NAME = "mog";
@@ -115,6 +116,7 @@ async function main() {
115
116
  console.log();
116
117
  console.log("Options:");
117
118
  console.log(" --include <file> — copy a file into the worktree (repeatable)");
119
+ console.log(" --fresh — ignore existing PR, start a brand new one");
118
120
  console.log();
119
121
  console.log("Example:");
120
122
  console.log(" mog init");
@@ -126,9 +128,10 @@ async function main() {
126
128
  return;
127
129
  }
128
130
 
129
- // Parse --include flags
131
+ // Parse --include and --fresh flags
130
132
  const includeFiles: string[] = [];
131
133
  const filteredArgs: string[] = [];
134
+ let fresh = false;
132
135
  for (let i = 0; i < args.length; i++) {
133
136
  if (args[i] === "--include" && i + 1 < args.length) {
134
137
  const filePath = path.resolve(args[i + 1]!);
@@ -137,6 +140,8 @@ async function main() {
137
140
  }
138
141
  includeFiles.push(filePath);
139
142
  i++; // skip the path argument
143
+ } else if (args[i] === "--fresh") {
144
+ fresh = true;
140
145
  } else {
141
146
  filteredArgs.push(args[i]!);
142
147
  }
@@ -170,6 +175,7 @@ async function main() {
170
175
  console.log();
171
176
  console.log("Options:");
172
177
  console.log(" --include <file> — copy a file into the worktree (repeatable)");
178
+ console.log(" --fresh — ignore existing PR, start a brand new one");
173
179
  console.log();
174
180
  console.log("Example:");
175
181
  console.log(" mog init");
@@ -214,6 +220,16 @@ async function main() {
214
220
  reposDir, owner, repoName, defaultBranch, issueNum, issue.title
215
221
  );
216
222
 
223
+ // Check for existing PR (unless --fresh)
224
+ let existingPR: PRFeedback | undefined;
225
+ if (!fresh) {
226
+ const pr = fetchPRFeedback(repo, branchName);
227
+ if (pr) {
228
+ existingPR = pr;
229
+ log.ok(`Found existing PR #${pr.prNumber} — will include review feedback and update it.`);
230
+ }
231
+ }
232
+
217
233
  // Copy included files into worktree
218
234
  const copiedFiles: string[] = [];
219
235
  for (const filePath of includeFiles) {
@@ -225,7 +241,8 @@ async function main() {
225
241
  }
226
242
 
227
243
  // Build prompts
228
- const planningPrompt = buildPlanningPrompt(repo, issueNum, issue);
244
+ const prFeedback = existingPR?.reviews || "";
245
+ const planningPrompt = buildPlanningPrompt(repo, issueNum, issue, prFeedback);
229
246
  const buildingPromptFn = (remaining: string[], plan: string) =>
230
247
  buildBuildingPrompt(repo, issueNum, issue, remaining, plan);
231
248
  const reviewPrompt = buildReviewPrompt(repo, issueNum, issue);
@@ -249,8 +266,8 @@ async function main() {
249
266
  }
250
267
  }
251
268
 
252
- // Push and create PR
253
- pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary);
269
+ // Push and create/update PR
270
+ pushAndCreatePR(repo, worktreeDir, branchName, defaultBranch, issueNum, issue, summary, existingPR);
254
271
  }
255
272
 
256
273
  function getReposDir(): string {
@@ -302,7 +319,7 @@ function tryRecoverSandbox(reposDir: string): boolean {
302
319
  return true;
303
320
  }
304
321
 
305
- function formatIssueContext(issueNum: string, issue: { title: string; body: string; labels: string; comments: string }): string {
322
+ function formatIssueContext(issueNum: string, issue: { title: string; body: string; labels: string; comments: string }, prFeedback?: string): string {
306
323
  let context = `## Issue: ${issue.title}
307
324
 
308
325
  ### Description
@@ -318,13 +335,22 @@ ${issue.labels}`;
318
335
  ${issue.comments}`;
319
336
  }
320
337
 
338
+ if (prFeedback) {
339
+ context += `
340
+
341
+ ### Previous PR Review Feedback
342
+ A previous attempt at this issue was reviewed. Address the following feedback in your implementation:
343
+
344
+ ${prFeedback}`;
345
+ }
346
+
321
347
  return context;
322
348
  }
323
349
 
324
- function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string; comments: string }): string {
350
+ function buildPlanningPrompt(repo: string, issueNum: string, issue: { title: string; body: string; labels: string; comments: string }, prFeedback?: string): string {
325
351
  return `You are working on GitHub issue #${issueNum} for the repository ${repo}.
326
352
 
327
- ${formatIssueContext(issueNum, issue)}
353
+ ${formatIssueContext(issueNum, issue, prFeedback)}
328
354
 
329
355
  ## Instructions
330
356