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 +20 -1
- package/dist/commands/clean.js +114 -33
- package/dist/utils/github.d.ts +9 -0
- package/dist/utils/github.js +29 -0
- package/package.json +1 -1
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
|
|
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
|
|
package/dist/commands/clean.js
CHANGED
|
@@ -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
|
|
189
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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)(
|
|
273
|
+
(0, child_process_1.execSync)(`/bin/rm -rf "${wt.path}"`, { stdio: 'pipe' });
|
|
231
274
|
}
|
|
232
275
|
catch {
|
|
233
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
|
|
416
|
+
// If folder still exists, force delete it
|
|
417
|
+
if (fs.existsSync(worktreePath)) {
|
|
350
418
|
try {
|
|
351
|
-
|
|
352
|
-
(0, child_process_1.execSync)(
|
|
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
|
-
|
|
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) {
|
package/dist/utils/github.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/github.js
CHANGED
|
@@ -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
|
+
}
|