@akiojin/gwt 2.9.1 → 2.11.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 +6 -0
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +4 -37
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/common/Input.d.ts +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -2
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +19 -14
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/ModelSelectorScreen.js +6 -6
- package/dist/cli/ui/components/screens/ModelSelectorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/PRCleanupScreen.js +0 -3
- package/dist/cli/ui/components/screens/PRCleanupScreen.js.map +1 -1
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +43 -2
- package/dist/cli/ui/hooks/useGitData.js.map +1 -1
- package/dist/cli/ui/types.d.ts +16 -4
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +124 -15
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
- package/dist/cli/ui/utils/modelOptions.js.map +1 -1
- package/dist/client/assets/{index-CNWntAlF.js → index-Dl798X5w.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/git.d.ts +6 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +40 -5
- package/dist/git.js.map +1 -1
- package/dist/web/client/src/components/BranchGraph.js +1 -1
- package/dist/web/client/src/components/BranchGraph.js.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.js +8 -3
- package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
- package/dist/web/server/routes/sessions.d.ts.map +1 -1
- package/dist/web/server/routes/sessions.js +4 -2
- package/dist/web/server/routes/sessions.js.map +1 -1
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +69 -62
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +9 -17
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +77 -83
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +107 -160
- package/src/cli/ui/__tests__/components/App.test.tsx +108 -84
- package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +12 -5
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +15 -14
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +3 -3
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +1 -4
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +31 -41
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +18 -17
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +3 -2
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +61 -41
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +15 -10
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +3 -2
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +0 -4
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +3 -5
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +24 -22
- package/src/cli/ui/components/App.tsx +5 -63
- package/src/cli/ui/components/common/Input.tsx +1 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +32 -22
- package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +46 -49
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +0 -3
- package/src/cli/ui/hooks/useGitData.ts +59 -1
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/types.ts +24 -5
- package/src/cli/ui/utils/branchFormatter.ts +123 -15
- package/src/cli/ui/utils/modelOptions.test.ts +4 -6
- package/src/cli/ui/utils/modelOptions.ts +1 -2
- package/src/config/index.ts +2 -1
- package/src/git.ts +56 -16
- package/src/index.test.ts +1 -1
- package/src/web/client/src/components/BranchGraph.tsx +1 -1
- package/src/web/client/src/pages/BranchDetailPage.tsx +8 -3
- package/src/web/server/routes/sessions.ts +12 -5
- package/src/worktree.ts +80 -91
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +0 -20
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts.map +0 -1
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +0 -65
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.js.map +0 -1
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +0 -151
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +0 -117
package/src/worktree.ts
CHANGED
|
@@ -6,10 +6,9 @@ import {
|
|
|
6
6
|
WorktreeConfig,
|
|
7
7
|
WorktreeWithPR,
|
|
8
8
|
CleanupTarget,
|
|
9
|
-
MergedPullRequest,
|
|
10
9
|
CleanupReason,
|
|
11
10
|
} from "./cli/ui/types.js";
|
|
12
|
-
import { getPullRequestByBranch
|
|
11
|
+
import { getPullRequestByBranch } from "./github.js";
|
|
13
12
|
import {
|
|
14
13
|
hasUncommittedChanges,
|
|
15
14
|
hasUnpushedCommits,
|
|
@@ -27,6 +26,23 @@ import { getConfig } from "./config/index.js";
|
|
|
27
26
|
import { GIT_CONFIG } from "./config/constants.js";
|
|
28
27
|
import { startSpinner } from "./utils/spinner.js";
|
|
29
28
|
|
|
29
|
+
async function getUpstreamBranch(branch: string): Promise<string | null> {
|
|
30
|
+
try {
|
|
31
|
+
const result = await execa("git", [
|
|
32
|
+
"rev-parse",
|
|
33
|
+
"--abbrev-ref",
|
|
34
|
+
`${branch}@{upstream}`,
|
|
35
|
+
]);
|
|
36
|
+
const stdout =
|
|
37
|
+
typeof (result as { stdout?: unknown })?.stdout === "string"
|
|
38
|
+
? (result as { stdout: string }).stdout.trim()
|
|
39
|
+
: "";
|
|
40
|
+
return stdout.length ? stdout : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
// Re-export WorktreeConfig for external use
|
|
31
47
|
export type { WorktreeConfig };
|
|
32
48
|
|
|
@@ -100,11 +116,9 @@ async function listWorktrees(): Promise<WorktreeInfo[]> {
|
|
|
100
116
|
try {
|
|
101
117
|
const { getRepositoryRoot } = await import("./git.js");
|
|
102
118
|
const repoRoot = await getRepositoryRoot();
|
|
103
|
-
const { stdout } = await execa(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ cwd: repoRoot },
|
|
107
|
-
);
|
|
119
|
+
const { stdout } = await execa("git", ["worktree", "list", "--porcelain"], {
|
|
120
|
+
cwd: repoRoot,
|
|
121
|
+
});
|
|
108
122
|
const worktrees: WorktreeInfo[] = [];
|
|
109
123
|
const lines = stdout.split("\n");
|
|
110
124
|
|
|
@@ -416,15 +430,13 @@ async function getWorktreesWithPRStatus(): Promise<WorktreeWithPR[]> {
|
|
|
416
430
|
}
|
|
417
431
|
|
|
418
432
|
/**
|
|
419
|
-
* worktree
|
|
433
|
+
* worktreeに存在しないローカルブランチのクリーンアップ候補を取得
|
|
420
434
|
* @returns {Promise<CleanupTarget[]>} クリーンアップ候補の配列
|
|
421
435
|
*/
|
|
422
436
|
async function getOrphanedLocalBranches({
|
|
423
|
-
mergedPRs,
|
|
424
437
|
baseBranch,
|
|
425
438
|
repoRoot,
|
|
426
439
|
}: {
|
|
427
|
-
mergedPRs: MergedPullRequest[];
|
|
428
440
|
baseBranch: string;
|
|
429
441
|
repoRoot: string;
|
|
430
442
|
}): Promise<CleanupTarget[]> {
|
|
@@ -469,7 +481,6 @@ async function getOrphanedLocalBranches({
|
|
|
469
481
|
|
|
470
482
|
// worktreeに存在しないローカルブランチのみ対象
|
|
471
483
|
if (!worktreeBranches.has(localBranch.name)) {
|
|
472
|
-
const mergedPR = findMatchingPR(localBranch.name, mergedPRs);
|
|
473
484
|
let hasUnpushed = false;
|
|
474
485
|
try {
|
|
475
486
|
hasUnpushed = await hasUnpushedCommitsInRepo(
|
|
@@ -482,42 +493,43 @@ async function getOrphanedLocalBranches({
|
|
|
482
493
|
|
|
483
494
|
const reasons: CleanupReason[] = [];
|
|
484
495
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
496
|
+
const upstreamBranch = await getUpstreamBranch(localBranch.name);
|
|
497
|
+
const comparisonBase = upstreamBranch ?? baseBranch;
|
|
488
498
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
);
|
|
499
|
+
const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
|
|
500
|
+
localBranch.name,
|
|
501
|
+
comparisonBase,
|
|
502
|
+
repoRoot,
|
|
503
|
+
);
|
|
495
504
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
505
|
+
if (!hasUniqueCommits) {
|
|
506
|
+
reasons.push("no-diff-with-base");
|
|
499
507
|
}
|
|
500
508
|
|
|
501
509
|
if (process.env.DEBUG_CLEANUP) {
|
|
502
510
|
console.log(
|
|
503
511
|
chalk.gray(
|
|
504
|
-
`Debug: Checking orphaned branch ${localBranch.name} ->
|
|
512
|
+
`Debug: Checking orphaned branch ${localBranch.name} -> reasons: ${reasons.join(", ")}`,
|
|
505
513
|
),
|
|
506
514
|
);
|
|
507
515
|
}
|
|
508
516
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
517
|
+
let hasRemoteBranch = false;
|
|
518
|
+
try {
|
|
519
|
+
hasRemoteBranch = await checkRemoteBranchExists(localBranch.name);
|
|
520
|
+
} catch {
|
|
521
|
+
hasRemoteBranch = false;
|
|
522
|
+
}
|
|
516
523
|
|
|
524
|
+
if (!hasUnpushed && hasRemoteBranch && hasUniqueCommits) {
|
|
525
|
+
reasons.push("remote-synced");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (reasons.length > 0) {
|
|
517
529
|
cleanupTargets.push({
|
|
518
530
|
worktreePath: null, // worktreeは存在しない
|
|
519
531
|
branch: localBranch.name,
|
|
520
|
-
pullRequest:
|
|
532
|
+
pullRequest: null,
|
|
521
533
|
hasUncommittedChanges: false, // worktreeが存在しないため常にfalse
|
|
522
534
|
hasUnpushedCommits: hasUnpushed,
|
|
523
535
|
cleanupType: "branch-only",
|
|
@@ -546,49 +558,19 @@ async function getOrphanedLocalBranches({
|
|
|
546
558
|
}
|
|
547
559
|
}
|
|
548
560
|
|
|
549
|
-
function normalizeBranchName(branchName: string): string {
|
|
550
|
-
return branchName
|
|
551
|
-
.replace(/^origin\//, "")
|
|
552
|
-
.replace(/^refs\/heads\//, "")
|
|
553
|
-
.replace(/^refs\/remotes\/origin\//, "")
|
|
554
|
-
.trim();
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function findMatchingPR(
|
|
558
|
-
worktreeBranch: string,
|
|
559
|
-
mergedPRs: MergedPullRequest[],
|
|
560
|
-
): MergedPullRequest | null {
|
|
561
|
-
const normalizedWorktreeBranch = normalizeBranchName(worktreeBranch);
|
|
562
|
-
|
|
563
|
-
for (const pr of mergedPRs) {
|
|
564
|
-
const normalizedPRBranch = normalizeBranchName(pr.branch);
|
|
565
|
-
|
|
566
|
-
if (normalizedWorktreeBranch === normalizedPRBranch) {
|
|
567
|
-
return pr;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
561
|
/**
|
|
575
562
|
* マージ済みPRに関連するworktreeおよびローカルブランチのクリーンアップ候補を取得
|
|
576
563
|
* @returns {Promise<CleanupTarget[]>} クリーンアップ候補の配列
|
|
577
564
|
*/
|
|
578
565
|
export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
579
|
-
const [config, repoRoot] = await Promise.all([
|
|
566
|
+
const [config, repoRoot, worktreesWithPR] = await Promise.all([
|
|
580
567
|
getConfig(),
|
|
581
568
|
getRepositoryRoot(),
|
|
569
|
+
getWorktreesWithPRStatus(),
|
|
582
570
|
]);
|
|
583
571
|
const baseBranch = config.defaultBaseBranch || GIT_CONFIG.DEFAULT_BASE_BRANCH;
|
|
584
572
|
|
|
585
|
-
// 並列実行で高速化 - worktreeとマージ済みPRの両方を取得
|
|
586
|
-
const [mergedPRs, worktreesWithPR] = await Promise.all([
|
|
587
|
-
getMergedPullRequests(),
|
|
588
|
-
getWorktreesWithPRStatus(),
|
|
589
|
-
]);
|
|
590
573
|
const orphanedBranches = await getOrphanedLocalBranches({
|
|
591
|
-
mergedPRs,
|
|
592
574
|
baseBranch,
|
|
593
575
|
repoRoot,
|
|
594
576
|
});
|
|
@@ -599,8 +581,6 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
599
581
|
worktreesWithPR.forEach((w) =>
|
|
600
582
|
console.log(` ${w.branch} -> ${w.worktreePath}`),
|
|
601
583
|
);
|
|
602
|
-
console.log(chalk.cyan("Debug: Merged PRs:"));
|
|
603
|
-
mergedPRs.forEach((pr) => console.log(` ${pr.branch} (PR #${pr.number})`));
|
|
604
584
|
}
|
|
605
585
|
|
|
606
586
|
for (const worktree of worktreesWithPR) {
|
|
@@ -614,26 +594,25 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
614
594
|
continue;
|
|
615
595
|
}
|
|
616
596
|
|
|
617
|
-
const mergedPR = findMatchingPR(worktree.branch, mergedPRs);
|
|
618
|
-
|
|
619
597
|
if (process.env.DEBUG_CLEANUP) {
|
|
620
|
-
|
|
621
|
-
console.log(
|
|
622
|
-
chalk.gray(
|
|
623
|
-
`Debug: Checking worktree ${worktree.branch} (normalized: ${normalizedWorktree}) -> ${mergedPR ? "MATCH" : "NO MATCH"}`,
|
|
624
|
-
),
|
|
625
|
-
);
|
|
598
|
+
console.log(chalk.gray(`Debug: Checking worktree ${worktree.branch}`));
|
|
626
599
|
}
|
|
627
600
|
|
|
628
601
|
const cleanupReasons: CleanupReason[] = [];
|
|
629
602
|
|
|
630
|
-
if (mergedPR) {
|
|
631
|
-
cleanupReasons.push("merged-pr");
|
|
632
|
-
}
|
|
633
|
-
|
|
634
603
|
// worktreeパスの存在を確認
|
|
635
604
|
const fs = await import("node:fs");
|
|
636
|
-
|
|
605
|
+
// Some test environments mock node:fs without existsSync on the module root.
|
|
606
|
+
const existsSync =
|
|
607
|
+
typeof fs.existsSync === "function"
|
|
608
|
+
? fs.existsSync
|
|
609
|
+
: typeof (fs as { default?: { existsSync?: unknown } }).default
|
|
610
|
+
?.existsSync === "function"
|
|
611
|
+
? (fs as { default: { existsSync: (p: string) => boolean } }).default
|
|
612
|
+
.existsSync
|
|
613
|
+
: null;
|
|
614
|
+
|
|
615
|
+
const isAccessible = existsSync ? existsSync(worktree.worktreePath) : false;
|
|
637
616
|
|
|
638
617
|
let hasUncommitted = false;
|
|
639
618
|
let hasUnpushed = false;
|
|
@@ -657,16 +636,28 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
657
636
|
}
|
|
658
637
|
}
|
|
659
638
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
639
|
+
let hasRemoteBranch = false;
|
|
640
|
+
try {
|
|
641
|
+
hasRemoteBranch = await checkRemoteBranchExists(worktree.branch);
|
|
642
|
+
} catch {
|
|
643
|
+
hasRemoteBranch = false;
|
|
644
|
+
}
|
|
666
645
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
646
|
+
const upstreamBranch = await getUpstreamBranch(worktree.branch);
|
|
647
|
+
const comparisonBase = upstreamBranch ?? baseBranch;
|
|
648
|
+
|
|
649
|
+
const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
|
|
650
|
+
worktree.branch,
|
|
651
|
+
comparisonBase,
|
|
652
|
+
repoRoot,
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// 差分がない場合はベース同等としてクリーンアップ候補
|
|
656
|
+
if (!hasUniqueCommits) {
|
|
657
|
+
cleanupReasons.push("no-diff-with-base");
|
|
658
|
+
} else if (!hasUncommitted && !hasUnpushed && hasRemoteBranch) {
|
|
659
|
+
// 未マージでも、ローカルに未コミット/未プッシュがなくリモートが最新ならローカルのみクリーンアップ許可
|
|
660
|
+
cleanupReasons.push("remote-synced");
|
|
670
661
|
}
|
|
671
662
|
|
|
672
663
|
if (process.env.DEBUG_CLEANUP) {
|
|
@@ -681,12 +672,10 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
681
672
|
continue;
|
|
682
673
|
}
|
|
683
674
|
|
|
684
|
-
const hasRemoteBranch = await checkRemoteBranchExists(worktree.branch);
|
|
685
|
-
|
|
686
675
|
const target: CleanupTarget = {
|
|
687
676
|
worktreePath: worktree.worktreePath,
|
|
688
677
|
branch: worktree.branch,
|
|
689
|
-
pullRequest:
|
|
678
|
+
pullRequest: null,
|
|
690
679
|
hasUncommittedChanges: hasUncommitted,
|
|
691
680
|
hasUnpushedCommits: hasUnpushed,
|
|
692
681
|
cleanupType: "worktree-and-branch",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
export interface WorktreeItem {
|
|
3
|
-
branch: string;
|
|
4
|
-
path: string;
|
|
5
|
-
isAccessible: boolean;
|
|
6
|
-
label?: string;
|
|
7
|
-
value?: string;
|
|
8
|
-
}
|
|
9
|
-
export interface WorktreeManagerScreenProps {
|
|
10
|
-
worktrees: WorktreeItem[];
|
|
11
|
-
onBack: () => void;
|
|
12
|
-
onSelect: (worktree: WorktreeItem) => void;
|
|
13
|
-
version?: string | null;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* WorktreeManagerScreen - Screen for managing worktrees
|
|
17
|
-
* Layout: Header + Stats + Worktree List + Footer
|
|
18
|
-
*/
|
|
19
|
-
export declare function WorktreeManagerScreen({ worktrees, onBack, onSelect, version, }: WorktreeManagerScreenProps): React.JSX.Element;
|
|
20
|
-
//# sourceMappingURL=WorktreeManagerScreen.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WorktreeManagerScreen.d.ts","sourceRoot":"","sources":["../../../../../src/cli/ui/components/screens/WorktreeManagerScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,GACR,EAAE,0BAA0B,qBAqF5B"}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import { Header } from "../parts/Header.js";
|
|
4
|
-
import { Footer } from "../parts/Footer.js";
|
|
5
|
-
import { Select } from "../common/Select.js";
|
|
6
|
-
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
7
|
-
/**
|
|
8
|
-
* WorktreeManagerScreen - Screen for managing worktrees
|
|
9
|
-
* Layout: Header + Stats + Worktree List + Footer
|
|
10
|
-
*/
|
|
11
|
-
export function WorktreeManagerScreen({ worktrees, onBack, onSelect, version, }) {
|
|
12
|
-
const { rows } = useTerminalSize();
|
|
13
|
-
// Handle keyboard input
|
|
14
|
-
// Note: Select component handles Enter and arrow keys
|
|
15
|
-
useInput((input, key) => {
|
|
16
|
-
if (key.escape) {
|
|
17
|
-
onBack();
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
// Calculate accessible and inaccessible counts
|
|
21
|
-
const accessibleCount = worktrees.filter((w) => w.isAccessible).length;
|
|
22
|
-
const inaccessibleCount = worktrees.filter((w) => !w.isAccessible).length;
|
|
23
|
-
// Format worktrees for Select component
|
|
24
|
-
const worktreeItems = worktrees.map((wt) => ({
|
|
25
|
-
...wt,
|
|
26
|
-
label: wt.isAccessible
|
|
27
|
-
? `${wt.branch} (${wt.path})`
|
|
28
|
-
: `${wt.branch} (${wt.path}) [Inaccessible]`,
|
|
29
|
-
value: wt.branch,
|
|
30
|
-
}));
|
|
31
|
-
// Calculate available space for worktree list
|
|
32
|
-
const headerLines = 2;
|
|
33
|
-
const statsLines = 1;
|
|
34
|
-
const emptyLine = 1;
|
|
35
|
-
const footerLines = 1;
|
|
36
|
-
const fixedLines = headerLines + statsLines + emptyLine + footerLines;
|
|
37
|
-
const contentHeight = rows - fixedLines;
|
|
38
|
-
const limit = Math.max(5, contentHeight);
|
|
39
|
-
// Footer actions
|
|
40
|
-
const footerActions = [
|
|
41
|
-
{ key: "enter", description: "Select" },
|
|
42
|
-
{ key: "esc", description: "Back" },
|
|
43
|
-
];
|
|
44
|
-
return (React.createElement(Box, { flexDirection: "column", height: rows },
|
|
45
|
-
React.createElement(Header, { title: "Worktree Manager", titleColor: "magenta", version: version }),
|
|
46
|
-
React.createElement(Box, { marginTop: 1 },
|
|
47
|
-
React.createElement(Box, { flexDirection: "row" },
|
|
48
|
-
React.createElement(Box, { marginRight: 2 },
|
|
49
|
-
React.createElement(Text, null,
|
|
50
|
-
"Total: ",
|
|
51
|
-
React.createElement(Text, { bold: true }, worktrees.length))),
|
|
52
|
-
React.createElement(Box, { marginRight: 2 },
|
|
53
|
-
React.createElement(Text, { color: "green" },
|
|
54
|
-
"Accessible: ",
|
|
55
|
-
React.createElement(Text, { bold: true }, accessibleCount))),
|
|
56
|
-
inaccessibleCount > 0 && (React.createElement(Box, null,
|
|
57
|
-
React.createElement(Text, { color: "red" },
|
|
58
|
-
"Inaccessible: ",
|
|
59
|
-
React.createElement(Text, { bold: true }, inaccessibleCount)))))),
|
|
60
|
-
React.createElement(Box, { height: 1 }),
|
|
61
|
-
React.createElement(Box, { flexDirection: "column", flexGrow: 1 }, worktrees.length === 0 ? (React.createElement(Box, null,
|
|
62
|
-
React.createElement(Text, { dimColor: true }, "No worktrees found"))) : (React.createElement(Select, { items: worktreeItems, onSelect: onSelect, limit: limit }))),
|
|
63
|
-
React.createElement(Footer, { actions: footerActions })));
|
|
64
|
-
}
|
|
65
|
-
//# sourceMappingURL=WorktreeManagerScreen.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WorktreeManagerScreen.js","sourceRoot":"","sources":["../../../../../src/cli/ui/components/screens/WorktreeManagerScreen.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vitest-environment happy-dom
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
5
|
-
import { render } from "@testing-library/react";
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { WorktreeManagerScreen } from "../../../components/screens/WorktreeManagerScreen.js";
|
|
8
|
-
import { Window } from "happy-dom";
|
|
9
|
-
|
|
10
|
-
describe("WorktreeManagerScreen", () => {
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
// Setup happy-dom
|
|
13
|
-
const window = new Window();
|
|
14
|
-
globalThis.window = window as any;
|
|
15
|
-
globalThis.document = window.document as any;
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const mockWorktrees = [
|
|
19
|
-
{
|
|
20
|
-
branch: "feature/test-1",
|
|
21
|
-
path: "/path/to/worktree-1",
|
|
22
|
-
isAccessible: true,
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
branch: "feature/test-2",
|
|
26
|
-
path: "/path/to/worktree-2",
|
|
27
|
-
isAccessible: true,
|
|
28
|
-
},
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
it("should render header with title", () => {
|
|
32
|
-
const onBack = vi.fn();
|
|
33
|
-
const onSelect = vi.fn();
|
|
34
|
-
const { getByText } = render(
|
|
35
|
-
<WorktreeManagerScreen
|
|
36
|
-
worktrees={mockWorktrees}
|
|
37
|
-
onBack={onBack}
|
|
38
|
-
onSelect={onSelect}
|
|
39
|
-
/>,
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
expect(getByText(/Worktree Manager/i)).toBeDefined();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("should render worktree list", () => {
|
|
46
|
-
const onBack = vi.fn();
|
|
47
|
-
const onSelect = vi.fn();
|
|
48
|
-
const { getByText } = render(
|
|
49
|
-
<WorktreeManagerScreen
|
|
50
|
-
worktrees={mockWorktrees}
|
|
51
|
-
onBack={onBack}
|
|
52
|
-
onSelect={onSelect}
|
|
53
|
-
/>,
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
expect(getByText(/feature\/test-1/)).toBeDefined();
|
|
57
|
-
expect(getByText(/feature\/test-2/)).toBeDefined();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("should render footer with actions", () => {
|
|
61
|
-
const onBack = vi.fn();
|
|
62
|
-
const onSelect = vi.fn();
|
|
63
|
-
const { getAllByText } = render(
|
|
64
|
-
<WorktreeManagerScreen
|
|
65
|
-
worktrees={mockWorktrees}
|
|
66
|
-
onBack={onBack}
|
|
67
|
-
onSelect={onSelect}
|
|
68
|
-
/>,
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
expect(getAllByText(/enter/i).length).toBeGreaterThan(0);
|
|
72
|
-
expect(getAllByText(/esc/i).length).toBeGreaterThan(0);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("should handle empty worktree list", () => {
|
|
76
|
-
const onBack = vi.fn();
|
|
77
|
-
const onSelect = vi.fn();
|
|
78
|
-
const { getByText } = render(
|
|
79
|
-
<WorktreeManagerScreen
|
|
80
|
-
worktrees={[]}
|
|
81
|
-
onBack={onBack}
|
|
82
|
-
onSelect={onSelect}
|
|
83
|
-
/>,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
expect(getByText(/No worktrees found/i)).toBeDefined();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("should display inaccessible worktrees differently", () => {
|
|
90
|
-
const worktreesWithInaccessible = [
|
|
91
|
-
{
|
|
92
|
-
branch: "feature/accessible",
|
|
93
|
-
path: "/path/accessible",
|
|
94
|
-
isAccessible: true,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
branch: "feature/inaccessible",
|
|
98
|
-
path: "/path/inaccessible",
|
|
99
|
-
isAccessible: false,
|
|
100
|
-
},
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
const onBack = vi.fn();
|
|
104
|
-
const onSelect = vi.fn();
|
|
105
|
-
const { getByText } = render(
|
|
106
|
-
<WorktreeManagerScreen
|
|
107
|
-
worktrees={worktreesWithInaccessible}
|
|
108
|
-
onBack={onBack}
|
|
109
|
-
onSelect={onSelect}
|
|
110
|
-
/>,
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
expect(getByText(/feature\/accessible/)).toBeDefined();
|
|
114
|
-
expect(getByText(/feature\/inaccessible/)).toBeDefined();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("should use terminal height for layout calculation", () => {
|
|
118
|
-
const originalRows = process.stdout.rows;
|
|
119
|
-
process.stdout.rows = 30;
|
|
120
|
-
|
|
121
|
-
const onBack = vi.fn();
|
|
122
|
-
const onSelect = vi.fn();
|
|
123
|
-
const { container } = render(
|
|
124
|
-
<WorktreeManagerScreen
|
|
125
|
-
worktrees={mockWorktrees}
|
|
126
|
-
onBack={onBack}
|
|
127
|
-
onSelect={onSelect}
|
|
128
|
-
/>,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
expect(container).toBeDefined();
|
|
132
|
-
|
|
133
|
-
process.stdout.rows = originalRows;
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("should display worktree count in stats", () => {
|
|
137
|
-
const onBack = vi.fn();
|
|
138
|
-
const onSelect = vi.fn();
|
|
139
|
-
const { getByText, getAllByText } = render(
|
|
140
|
-
<WorktreeManagerScreen
|
|
141
|
-
worktrees={mockWorktrees}
|
|
142
|
-
onBack={onBack}
|
|
143
|
-
onSelect={onSelect}
|
|
144
|
-
/>,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// Check for worktree count
|
|
148
|
-
expect(getByText(/Total:/i)).toBeDefined();
|
|
149
|
-
expect(getAllByText(/2/).length).toBeGreaterThan(0);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import { Header } from "../parts/Header.js";
|
|
4
|
-
import { Footer } from "../parts/Footer.js";
|
|
5
|
-
import { Select } from "../common/Select.js";
|
|
6
|
-
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
7
|
-
|
|
8
|
-
export interface WorktreeItem {
|
|
9
|
-
branch: string;
|
|
10
|
-
path: string;
|
|
11
|
-
isAccessible: boolean;
|
|
12
|
-
label?: string;
|
|
13
|
-
value?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface WorktreeManagerScreenProps {
|
|
17
|
-
worktrees: WorktreeItem[];
|
|
18
|
-
onBack: () => void;
|
|
19
|
-
onSelect: (worktree: WorktreeItem) => void;
|
|
20
|
-
version?: string | null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* WorktreeManagerScreen - Screen for managing worktrees
|
|
25
|
-
* Layout: Header + Stats + Worktree List + Footer
|
|
26
|
-
*/
|
|
27
|
-
export function WorktreeManagerScreen({
|
|
28
|
-
worktrees,
|
|
29
|
-
onBack,
|
|
30
|
-
onSelect,
|
|
31
|
-
version,
|
|
32
|
-
}: WorktreeManagerScreenProps) {
|
|
33
|
-
const { rows } = useTerminalSize();
|
|
34
|
-
|
|
35
|
-
// Handle keyboard input
|
|
36
|
-
// Note: Select component handles Enter and arrow keys
|
|
37
|
-
useInput((input, key) => {
|
|
38
|
-
if (key.escape) {
|
|
39
|
-
onBack();
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Calculate accessible and inaccessible counts
|
|
44
|
-
const accessibleCount = worktrees.filter((w) => w.isAccessible).length;
|
|
45
|
-
const inaccessibleCount = worktrees.filter((w) => !w.isAccessible).length;
|
|
46
|
-
|
|
47
|
-
// Format worktrees for Select component
|
|
48
|
-
const worktreeItems = worktrees.map((wt) => ({
|
|
49
|
-
...wt,
|
|
50
|
-
label: wt.isAccessible
|
|
51
|
-
? `${wt.branch} (${wt.path})`
|
|
52
|
-
: `${wt.branch} (${wt.path}) [Inaccessible]`,
|
|
53
|
-
value: wt.branch,
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
// Calculate available space for worktree list
|
|
57
|
-
const headerLines = 2;
|
|
58
|
-
const statsLines = 1;
|
|
59
|
-
const emptyLine = 1;
|
|
60
|
-
const footerLines = 1;
|
|
61
|
-
const fixedLines = headerLines + statsLines + emptyLine + footerLines;
|
|
62
|
-
const contentHeight = rows - fixedLines;
|
|
63
|
-
const limit = Math.max(5, contentHeight);
|
|
64
|
-
|
|
65
|
-
// Footer actions
|
|
66
|
-
const footerActions = [
|
|
67
|
-
{ key: "enter", description: "Select" },
|
|
68
|
-
{ key: "esc", description: "Back" },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<Box flexDirection="column" height={rows}>
|
|
73
|
-
{/* Header */}
|
|
74
|
-
<Header title="Worktree Manager" titleColor="magenta" version={version} />
|
|
75
|
-
|
|
76
|
-
{/* Stats */}
|
|
77
|
-
<Box marginTop={1}>
|
|
78
|
-
<Box flexDirection="row">
|
|
79
|
-
<Box marginRight={2}>
|
|
80
|
-
<Text>
|
|
81
|
-
Total: <Text bold>{worktrees.length}</Text>
|
|
82
|
-
</Text>
|
|
83
|
-
</Box>
|
|
84
|
-
<Box marginRight={2}>
|
|
85
|
-
<Text color="green">
|
|
86
|
-
Accessible: <Text bold>{accessibleCount}</Text>
|
|
87
|
-
</Text>
|
|
88
|
-
</Box>
|
|
89
|
-
{inaccessibleCount > 0 && (
|
|
90
|
-
<Box>
|
|
91
|
-
<Text color="red">
|
|
92
|
-
Inaccessible: <Text bold>{inaccessibleCount}</Text>
|
|
93
|
-
</Text>
|
|
94
|
-
</Box>
|
|
95
|
-
)}
|
|
96
|
-
</Box>
|
|
97
|
-
</Box>
|
|
98
|
-
|
|
99
|
-
{/* Empty line */}
|
|
100
|
-
<Box height={1} />
|
|
101
|
-
|
|
102
|
-
{/* Content */}
|
|
103
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
104
|
-
{worktrees.length === 0 ? (
|
|
105
|
-
<Box>
|
|
106
|
-
<Text dimColor>No worktrees found</Text>
|
|
107
|
-
</Box>
|
|
108
|
-
) : (
|
|
109
|
-
<Select items={worktreeItems} onSelect={onSelect} limit={limit} />
|
|
110
|
-
)}
|
|
111
|
-
</Box>
|
|
112
|
-
|
|
113
|
-
{/* Footer */}
|
|
114
|
-
<Footer actions={footerActions} />
|
|
115
|
-
</Box>
|
|
116
|
-
);
|
|
117
|
-
}
|