@simonfestl/husky-cli 0.9.5 → 0.9.6

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.
@@ -33,6 +33,11 @@ export interface MergeOptions {
33
33
  deleteAfter?: boolean;
34
34
  message?: string;
35
35
  }
36
+ export interface ConflictCheckResult {
37
+ hasConflicts: boolean;
38
+ conflictFiles: string[];
39
+ checkedAt: Date;
40
+ }
36
41
  export declare class WorktreeManager {
37
42
  private projectDir;
38
43
  private baseBranch;
@@ -130,4 +135,25 @@ export declare class WorktreeManager {
130
135
  * Get the worktrees directory.
131
136
  */
132
137
  getWorktreesDir(): string;
138
+ /**
139
+ * Check if a worktree branch would have merge conflicts with base branch.
140
+ * Uses git merge-tree to simulate the merge without actually doing it.
141
+ */
142
+ checkMergeConflicts(sessionName: string): ConflictCheckResult;
143
+ /**
144
+ * Push the worktree branch to remote.
145
+ */
146
+ pushWorktreeBranch(sessionName: string, force?: boolean): boolean;
147
+ /**
148
+ * Create a PR for a worktree branch using gh CLI.
149
+ */
150
+ createPullRequest(sessionName: string, options: {
151
+ title: string;
152
+ body?: string;
153
+ draft?: boolean;
154
+ }): {
155
+ success: boolean;
156
+ prUrl?: string;
157
+ error?: string;
158
+ };
133
159
  }
@@ -470,4 +470,131 @@ export class WorktreeManager {
470
470
  getWorktreesDir() {
471
471
  return this.worktreesDir;
472
472
  }
473
+ /**
474
+ * Check if a worktree branch would have merge conflicts with base branch.
475
+ * Uses git merge-tree to simulate the merge without actually doing it.
476
+ */
477
+ checkMergeConflicts(sessionName) {
478
+ const worktreePath = this.getWorktreePath(sessionName);
479
+ const branchName = this.getBranchName(sessionName);
480
+ if (!fs.existsSync(worktreePath)) {
481
+ return {
482
+ hasConflicts: false,
483
+ conflictFiles: [],
484
+ checkedAt: new Date(),
485
+ };
486
+ }
487
+ // Get the merge base
488
+ const mergeBaseResult = this.runGit([
489
+ "merge-base",
490
+ this.baseBranch,
491
+ branchName,
492
+ ]);
493
+ if (mergeBaseResult.status !== 0) {
494
+ // No common ancestor, can't check conflicts
495
+ return {
496
+ hasConflicts: false,
497
+ conflictFiles: [],
498
+ checkedAt: new Date(),
499
+ };
500
+ }
501
+ const mergeBase = mergeBaseResult.stdout.trim();
502
+ // Use git merge-tree to simulate the merge
503
+ const mergeTreeResult = this.runGit([
504
+ "merge-tree",
505
+ mergeBase,
506
+ this.baseBranch,
507
+ branchName,
508
+ ]);
509
+ // Parse the output for conflicts
510
+ const output = mergeTreeResult.stdout;
511
+ const conflictFiles = [];
512
+ // Look for conflict markers in merge-tree output
513
+ // Format: "changed in both" or conflict sections
514
+ const lines = output.split("\n");
515
+ let inConflict = false;
516
+ for (const line of lines) {
517
+ if (line.includes("changed in both") || line.includes("CONFLICT")) {
518
+ inConflict = true;
519
+ }
520
+ // Extract file paths from conflict sections
521
+ if (inConflict && line.match(/^\+\+\+|^---|^@@/)) {
522
+ const fileMatch = line.match(/^\+\+\+ b\/(.+)$/) || line.match(/^--- a\/(.+)$/);
523
+ if (fileMatch && fileMatch[1] && !conflictFiles.includes(fileMatch[1])) {
524
+ conflictFiles.push(fileMatch[1]);
525
+ }
526
+ }
527
+ }
528
+ // Alternative: use diff to find files changed in both branches
529
+ if (conflictFiles.length === 0 && output.includes("<<<<<<<")) {
530
+ // Parse conflict markers directly
531
+ const conflictMatches = output.matchAll(/\+\+\+ b\/([^\n]+)/g);
532
+ for (const match of conflictMatches) {
533
+ if (match[1] && !conflictFiles.includes(match[1])) {
534
+ conflictFiles.push(match[1]);
535
+ }
536
+ }
537
+ }
538
+ return {
539
+ hasConflicts: conflictFiles.length > 0 || output.includes("<<<<<<<") || output.includes("CONFLICT"),
540
+ conflictFiles,
541
+ checkedAt: new Date(),
542
+ };
543
+ }
544
+ /**
545
+ * Push the worktree branch to remote.
546
+ */
547
+ pushWorktreeBranch(sessionName, force = false) {
548
+ const branchName = this.getBranchName(sessionName);
549
+ const pushArgs = ["push", "-u", "origin", branchName];
550
+ if (force) {
551
+ pushArgs.splice(1, 0, "--force");
552
+ }
553
+ const result = this.runGit(pushArgs);
554
+ if (result.status !== 0) {
555
+ console.error(`Failed to push branch: ${result.stderr}`);
556
+ return false;
557
+ }
558
+ console.log(`Pushed ${branchName} to origin`);
559
+ return true;
560
+ }
561
+ /**
562
+ * Create a PR for a worktree branch using gh CLI.
563
+ */
564
+ createPullRequest(sessionName, options) {
565
+ const branchName = this.getBranchName(sessionName);
566
+ // Check if gh CLI is available
567
+ const ghCheck = spawnSync("which", ["gh"], { encoding: "utf-8" });
568
+ if (ghCheck.status !== 0) {
569
+ return { success: false, error: "GitHub CLI (gh) not installed" };
570
+ }
571
+ // Create PR
572
+ const prArgs = [
573
+ "pr",
574
+ "create",
575
+ "--base",
576
+ this.baseBranch,
577
+ "--head",
578
+ branchName,
579
+ "--title",
580
+ options.title,
581
+ ];
582
+ if (options.body) {
583
+ prArgs.push("--body", options.body);
584
+ }
585
+ if (options.draft) {
586
+ prArgs.push("--draft");
587
+ }
588
+ const result = spawnSync("gh", prArgs, {
589
+ cwd: this.projectDir,
590
+ encoding: "utf-8",
591
+ timeout: 60000,
592
+ });
593
+ if (result.status !== 0) {
594
+ return { success: false, error: result.stderr || "Failed to create PR" };
595
+ }
596
+ // Extract PR URL from output
597
+ const prUrl = result.stdout.trim();
598
+ return { success: true, prUrl };
599
+ }
473
600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {