claude-issue-solver 1.6.5 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,18 @@ Automatically solve GitHub issues using [Claude Code](https://claude.ai/code).
4
4
 
5
5
  This CLI tool fetches an issue from your repo, creates a worktree, opens Claude Code in a new terminal to solve it, and creates a PR when done.
6
6
 
7
+ > **⚠️ DISCLAIMER: USE AT YOUR OWN RISK**
8
+ >
9
+ > This tool runs Claude Code with the `--dangerously-skip-permissions` flag, which allows Claude to execute commands and modify files **without asking for confirmation**. This is powerful but potentially risky.
10
+ >
11
+ > **Before using this tool:**
12
+ > - Understand that Claude will have unrestricted access to your codebase
13
+ > - Review what Claude is doing in the terminal
14
+ > - Use git to review changes before merging PRs
15
+ > - Never run this on production systems or sensitive repositories without careful consideration
16
+ >
17
+ > By using this tool, you accept full responsibility for any changes made to your code.
18
+
7
19
  ## Demo
8
20
 
9
21
  ```bash
@@ -78,6 +90,12 @@ claude-issue pr 42
78
90
  # Clean up worktree and branch after PR is merged
79
91
  claude-issue clean 42
80
92
 
93
+ # Clean all worktrees (shows PR/issue status)
94
+ claude-issue clean
95
+
96
+ # Navigate to a worktree or open its PR
97
+ claude-issue go
98
+
81
99
  # Show help
82
100
  claude-issue --help
83
101
  ```
@@ -135,8 +153,9 @@ claude-issue --help
135
153
 
136
154
  - Use `/exit` in Claude to end the session and trigger PR creation
137
155
  - Worktrees share the same `.git` so commits are visible in main repo
138
- - Run `claude-issue clean <number>` after merging to clean up
156
+ - Run `claude-issue clean` after merging to clean up - it shows PR status (merged/open/closed)
139
157
  - You can work on multiple issues in parallel - each gets its own worktree
158
+ - Use `claude-issue go` to quickly navigate to worktrees or open PRs in browser
140
159
 
141
160
  ## License
142
161
 
@@ -45,6 +45,7 @@ const fs = __importStar(require("fs"));
45
45
  const path = __importStar(require("path"));
46
46
  const os = __importStar(require("os"));
47
47
  const child_process_1 = require("child_process");
48
+ const github_1 = require("../utils/github");
48
49
  const git_1 = require("../utils/git");
49
50
  function closeWindowsWithPath(folderPath, issueNumber) {
50
51
  if (os.platform() !== 'darwin')
@@ -119,6 +120,30 @@ function closeWindowsWithPath(folderPath, issueNumber) {
119
120
  // VS Code not running or no matching windows
120
121
  }
121
122
  }
123
+ function getStatusLabel(wt) {
124
+ if (!wt.branch) {
125
+ return chalk_1.default.yellow('(orphaned folder)');
126
+ }
127
+ if (wt.prStatus) {
128
+ switch (wt.prStatus.state) {
129
+ case 'merged':
130
+ return chalk_1.default.green('✓ PR merged');
131
+ case 'open':
132
+ return chalk_1.default.blue('◐ PR open');
133
+ case 'closed':
134
+ return chalk_1.default.red('✗ PR closed');
135
+ }
136
+ }
137
+ if (wt.issueStatus) {
138
+ switch (wt.issueStatus.state) {
139
+ case 'closed':
140
+ return chalk_1.default.dim('● Issue closed');
141
+ case 'open':
142
+ return chalk_1.default.cyan('○ Issue open');
143
+ }
144
+ }
145
+ return chalk_1.default.dim('? Unknown');
146
+ }
122
147
  function getIssueWorktrees() {
123
148
  const projectRoot = (0, git_1.getProjectRoot)();
124
149
  const projectName = (0, git_1.getProjectName)();
@@ -184,9 +209,21 @@ async function cleanAllCommand() {
184
209
  console.log(chalk_1.default.yellow('\nNo issue worktrees found.'));
185
210
  return;
186
211
  }
212
+ // Fetch status for all worktrees
213
+ const statusSpinner = (0, ora_1.default)('Fetching issue and PR status...').start();
214
+ const worktreesWithStatus = worktrees.map((wt) => ({
215
+ ...wt,
216
+ issueStatus: (0, github_1.getIssueStatus)(parseInt(wt.issueNumber, 10)),
217
+ prStatus: wt.branch ? (0, github_1.getPRForBranch)(wt.branch) : null,
218
+ }));
219
+ statusSpinner.stop();
187
220
  console.log(chalk_1.default.bold('\n🧹 Found issue worktrees:\n'));
188
- for (const wt of worktrees) {
189
- console.log(` ${chalk_1.default.cyan(`#${wt.issueNumber}`)}\t${wt.branch || chalk_1.default.yellow('(orphaned folder)')}`);
221
+ for (const wt of worktreesWithStatus) {
222
+ const status = getStatusLabel(wt);
223
+ console.log(` ${chalk_1.default.cyan(`#${wt.issueNumber}`)}\t${status}`);
224
+ if (wt.branch) {
225
+ console.log(chalk_1.default.dim(` \t${wt.branch}`));
226
+ }
190
227
  console.log(chalk_1.default.dim(` \t${wt.path}`));
191
228
  console.log();
192
229
  }
@@ -215,25 +252,42 @@ async function cleanAllCommand() {
215
252
  catch {
216
253
  // Ignore errors closing windows
217
254
  }
218
- // Remove worktree
255
+ // Remove worktree/folder
256
+ const isOrphaned = !wt.branch;
219
257
  if (fs.existsSync(wt.path)) {
220
- try {
221
- (0, child_process_1.execSync)(`git worktree remove "${wt.path}" --force`, {
222
- cwd: projectRoot,
223
- stdio: 'pipe',
224
- });
258
+ // Try git worktree remove first (only if not orphaned)
259
+ if (!isOrphaned) {
260
+ try {
261
+ (0, child_process_1.execSync)(`git worktree remove "${wt.path}" --force`, {
262
+ cwd: projectRoot,
263
+ stdio: 'pipe',
264
+ });
265
+ }
266
+ catch {
267
+ // Ignore - we'll force delete below if needed
268
+ }
225
269
  }
226
- catch {
227
- // If git worktree remove fails, try removing directory manually with rm -rf
228
- // This handles locked files better than fs.rmSync
270
+ // If folder still exists, force delete it
271
+ if (fs.existsSync(wt.path)) {
229
272
  try {
230
- (0, child_process_1.execSync)(`rm -rf "${wt.path}"`, { stdio: 'pipe' });
273
+ (0, child_process_1.execSync)(`/bin/rm -rf "${wt.path}"`, { stdio: 'pipe' });
231
274
  }
232
275
  catch {
233
- fs.rmSync(wt.path, { recursive: true, force: true });
276
+ try {
277
+ fs.rmSync(wt.path, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
278
+ }
279
+ catch {
280
+ // Ignore - will check at end
281
+ }
234
282
  }
283
+ }
284
+ // Prune git worktrees
285
+ try {
235
286
  (0, child_process_1.execSync)('git worktree prune', { cwd: projectRoot, stdio: 'pipe' });
236
287
  }
288
+ catch {
289
+ // Ignore
290
+ }
237
291
  }
238
292
  // Delete branch (if we have one)
239
293
  if (wt.branch) {
@@ -302,12 +356,21 @@ async function cleanCommand(issueNumber) {
302
356
  const branchName = worktree.branch;
303
357
  const worktreePath = worktree.path;
304
358
  const isOrphaned = !branchName;
359
+ // Fetch status
360
+ const statusSpinner = (0, ora_1.default)('Fetching issue and PR status...').start();
361
+ const issueStatus = (0, github_1.getIssueStatus)(issueNumber);
362
+ const prStatus = branchName ? (0, github_1.getPRForBranch)(branchName) : null;
363
+ statusSpinner.stop();
364
+ const wtWithStatus = {
365
+ ...worktree,
366
+ issueStatus,
367
+ prStatus,
368
+ };
369
+ const status = getStatusLabel(wtWithStatus);
305
370
  console.log();
306
371
  console.log(chalk_1.default.bold(`🧹 Cleaning up issue #${issueNumber}`));
307
- if (isOrphaned) {
308
- console.log(chalk_1.default.yellow(` (Orphaned folder - no git worktree reference)`));
309
- }
310
- else {
372
+ console.log(` Status: ${status}`);
373
+ if (!isOrphaned) {
311
374
  console.log(chalk_1.default.dim(` Branch: ${branchName}`));
312
375
  }
313
376
  console.log(chalk_1.default.dim(` Folder: ${worktreePath}`));
@@ -335,34 +398,52 @@ async function cleanCommand(issueNumber) {
335
398
  catch {
336
399
  windowSpinner.warn('Could not close some windows');
337
400
  }
338
- // Remove worktree
401
+ // Remove worktree/folder
339
402
  if (fs.existsSync(worktreePath)) {
340
403
  const worktreeSpinner = (0, ora_1.default)('Removing worktree...').start();
341
- try {
342
- (0, child_process_1.execSync)(`git worktree remove "${worktreePath}" --force`, {
343
- cwd: projectRoot,
344
- stdio: 'pipe',
345
- });
346
- worktreeSpinner.succeed('Worktree removed');
404
+ // Try git worktree remove first (only if not orphaned)
405
+ if (!isOrphaned) {
406
+ try {
407
+ (0, child_process_1.execSync)(`git worktree remove "${worktreePath}" --force`, {
408
+ cwd: projectRoot,
409
+ stdio: 'pipe',
410
+ });
411
+ }
412
+ catch {
413
+ // Ignore - we'll force delete below if needed
414
+ }
347
415
  }
348
- catch {
349
- // If git worktree remove fails, try removing directory manually with rm -rf
416
+ // If folder still exists, force delete it
417
+ if (fs.existsSync(worktreePath)) {
350
418
  try {
351
- (0, child_process_1.execSync)(`rm -rf "${worktreePath}"`, { stdio: 'pipe' });
352
- (0, child_process_1.execSync)('git worktree prune', { cwd: projectRoot, stdio: 'pipe' });
353
- worktreeSpinner.succeed('Worktree removed (manually)');
419
+ // Use rm -rf with full path
420
+ (0, child_process_1.execSync)(`/bin/rm -rf "${worktreePath}"`, { stdio: 'pipe' });
354
421
  }
355
422
  catch {
423
+ // Try Node's rmSync as fallback
356
424
  try {
357
- fs.rmSync(worktreePath, { recursive: true, force: true });
358
- (0, child_process_1.execSync)('git worktree prune', { cwd: projectRoot, stdio: 'pipe' });
359
- worktreeSpinner.succeed('Worktree removed (manually)');
425
+ fs.rmSync(worktreePath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
360
426
  }
361
427
  catch {
362
- worktreeSpinner.warn('Could not remove worktree directory');
428
+ // Last resort - try with sudo hint
429
+ worktreeSpinner.warn(`Could not remove directory. Try manually: rm -rf "${worktreePath}"`);
363
430
  }
364
431
  }
365
432
  }
433
+ // Prune git worktrees
434
+ try {
435
+ (0, child_process_1.execSync)('git worktree prune', { cwd: projectRoot, stdio: 'pipe' });
436
+ }
437
+ catch {
438
+ // Ignore
439
+ }
440
+ // Check final result
441
+ if (fs.existsSync(worktreePath)) {
442
+ worktreeSpinner.warn(`Could not fully remove directory: ${worktreePath}`);
443
+ }
444
+ else {
445
+ worktreeSpinner.succeed(isOrphaned ? 'Folder removed' : 'Worktree removed');
446
+ }
366
447
  }
367
448
  // Delete branch (if we have one)
368
449
  if (branchName) {
@@ -11,3 +11,12 @@ export interface IssueListItem {
11
11
  export declare function getIssue(issueNumber: number): Issue | null;
12
12
  export declare function listIssues(limit?: number): IssueListItem[];
13
13
  export declare function createPullRequest(title: string, body: string, branch: string, base?: string): string;
14
+ export interface IssueStatus {
15
+ state: 'open' | 'closed';
16
+ }
17
+ export interface PRStatus {
18
+ state: 'open' | 'closed' | 'merged';
19
+ url: string;
20
+ }
21
+ export declare function getIssueStatus(issueNumber: number): IssueStatus | null;
22
+ export declare function getPRForBranch(branch: string): PRStatus | null;
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getIssue = getIssue;
4
4
  exports.listIssues = listIssues;
5
5
  exports.createPullRequest = createPullRequest;
6
+ exports.getIssueStatus = getIssueStatus;
7
+ exports.getPRForBranch = getPRForBranch;
6
8
  const child_process_1 = require("child_process");
7
9
  function getIssue(issueNumber) {
8
10
  try {
@@ -32,3 +34,30 @@ function createPullRequest(title, body, branch, base = 'main') {
32
34
  const output = (0, child_process_1.execSync)(`gh pr create --title "${title}" --body "${body.replace(/"/g, '\\"')}" --head "${branch}" --base "${base}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
33
35
  return output.trim();
34
36
  }
37
+ function getIssueStatus(issueNumber) {
38
+ try {
39
+ const output = (0, child_process_1.execSync)(`gh issue view ${issueNumber} --json state`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
40
+ const data = JSON.parse(output);
41
+ return {
42
+ state: data.state.toLowerCase(),
43
+ };
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function getPRForBranch(branch) {
50
+ try {
51
+ const output = (0, child_process_1.execSync)(`gh pr list --head "${branch}" --state all --json state,url --limit 1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
52
+ const data = JSON.parse(output);
53
+ if (data.length === 0)
54
+ return null;
55
+ return {
56
+ state: data[0].state.toLowerCase(),
57
+ url: data[0].url,
58
+ };
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-issue-solver",
3
- "version": "1.6.5",
3
+ "version": "1.7.0",
4
4
  "description": "Automatically solve GitHub issues using Claude Code",
5
5
  "main": "dist/index.js",
6
6
  "bin": {