worktree-launcher 1.3.0 → 1.4.1

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.
Files changed (3) hide show
  1. package/README.md +14 -5
  2. package/dist/index.js +381 -70
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,26 +38,35 @@ Requires Node.js 18+ and git.
38
38
 
39
39
  ## Interactive Mode (TUI)
40
40
 
41
- Run `wt` with no arguments to open a terminal UI similar to lazygit:
41
+ Run `wt` with no arguments to open a terminal UI:
42
42
 
43
43
  ```bash
44
44
  wt
45
45
  ```
46
46
 
47
- The TUI displays all worktrees in a navigable list with keyboard shortcuts:
47
+ The TUI shows your repo name, current branch, and all existing worktrees. Use keyboard shortcuts to manage them:
48
48
 
49
49
  | Key | Action |
50
50
  |-----|--------|
51
- | `n` | Create new worktree |
51
+ | `n` | Create new worktree (guided wizard) |
52
52
  | `d` | Delete selected worktree |
53
53
  | `c` | Launch Claude Code in selected worktree |
54
54
  | `x` | Launch Codex in selected worktree |
55
- | `p` | Push branch to remote |
56
55
  | `Enter` | Print cd command for selected worktree |
57
56
  | `r` | Refresh worktree list |
58
57
  | `q` | Quit |
59
58
 
60
- Navigate with arrow keys or vim-style `j`/`k`. The status bar shows the full path of the selected worktree.
59
+ Navigate with arrow keys or vim-style `j`/`k`.
60
+
61
+ ### Creating a New Worktree
62
+
63
+ Press `n` to start the guided creation wizard:
64
+
65
+ 1. **Branch name** - Enter the name for your new branch
66
+ 2. **Base branch** - Create from current branch or default branch (main/master)
67
+ 3. **Copy .env files** - Copy environment files to the new worktree
68
+ 4. **Push to remote** - Push branch to GitHub immediately
69
+ 5. **Launch AI tool** - Choose Claude Code, Codex, or skip
61
70
 
62
71
  ## Commands
63
72
 
package/dist/index.js CHANGED
@@ -44,6 +44,14 @@ async function remoteBranchExists(branchName) {
44
44
  return false;
45
45
  }
46
46
  }
47
+ async function getCurrentBranch() {
48
+ try {
49
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
50
+ return stdout.trim();
51
+ } catch {
52
+ return "HEAD";
53
+ }
54
+ }
47
55
  async function getDefaultBranch() {
48
56
  try {
49
57
  const { stdout } = await execFileAsync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"]);
@@ -103,8 +111,8 @@ async function pruneWorktrees() {
103
111
  }
104
112
  async function isBranchMerged(branchName) {
105
113
  try {
106
- const defaultBranch = await getDefaultBranch();
107
- const { stdout } = await execFileAsync("git", ["branch", "--merged", defaultBranch]);
114
+ const defaultBranch2 = await getDefaultBranch();
115
+ const { stdout } = await execFileAsync("git", ["branch", "--merged", defaultBranch2]);
108
116
  const mergedBranches = stdout.split("\n").map((b) => b.trim().replace("* ", ""));
109
117
  return mergedBranches.includes(branchName);
110
118
  } catch {
@@ -602,7 +610,10 @@ var screen;
602
610
  var worktreeList;
603
611
  var statusBar;
604
612
  var helpBar;
613
+ var headerBox;
605
614
  var mainRepoPath;
615
+ var currentBranch;
616
+ var defaultBranch;
606
617
  var worktrees = [];
607
618
  var selectedIndex = 0;
608
619
  async function interactiveCommand() {
@@ -611,22 +622,26 @@ async function interactiveCommand() {
611
622
  process.exit(1);
612
623
  }
613
624
  mainRepoPath = await getGitRoot();
625
+ currentBranch = await getCurrentBranch();
626
+ defaultBranch = await getDefaultBranch();
614
627
  const repoName = path8.basename(mainRepoPath);
615
628
  screen = blessed.screen({
616
629
  smartCSR: true,
617
- title: `wt - ${repoName}`
630
+ fullUnicode: true,
631
+ title: `wt - ${repoName}`,
632
+ terminal: "xterm-256color",
633
+ warnings: false
618
634
  });
619
- blessed.box({
635
+ headerBox = blessed.box({
620
636
  parent: screen,
621
637
  top: 0,
622
638
  left: 0,
623
639
  width: "100%",
624
640
  height: 1,
625
- content: ` Worktrees: ${repoName}`,
641
+ content: ` ${repoName} (${currentBranch})`,
626
642
  style: {
627
- fg: "white",
628
- bg: "blue",
629
- bold: true
643
+ fg: "black",
644
+ bg: "cyan"
630
645
  }
631
646
  });
632
647
  worktreeList = blessed.list({
@@ -640,16 +655,16 @@ async function interactiveCommand() {
640
655
  mouse: true,
641
656
  style: {
642
657
  selected: {
643
- bg: "blue",
644
- fg: "white"
658
+ bg: "cyan",
659
+ fg: "black"
645
660
  },
646
661
  item: {
647
- fg: "white"
662
+ fg: "default"
648
663
  }
649
664
  },
650
665
  scrollbar: {
651
666
  ch: " ",
652
- style: { bg: "grey" }
667
+ style: { bg: "cyan" }
653
668
  }
654
669
  });
655
670
  statusBar = blessed.box({
@@ -660,8 +675,7 @@ async function interactiveCommand() {
660
675
  height: 1,
661
676
  content: "",
662
677
  style: {
663
- fg: "yellow",
664
- bg: "black"
678
+ fg: "green"
665
679
  }
666
680
  });
667
681
  helpBar = blessed.box({
@@ -670,10 +684,10 @@ async function interactiveCommand() {
670
684
  left: 0,
671
685
  width: "100%",
672
686
  height: 1,
673
- content: " [n]ew [d]elete [c]laude [x]codex [p]ush [Enter]cd [q]uit",
687
+ content: " [n]ew [d]elete [c]laude [x]codex [Enter]cd [q]uit",
674
688
  style: {
675
689
  fg: "black",
676
- bg: "white"
690
+ bg: "cyan"
677
691
  }
678
692
  });
679
693
  await refreshWorktrees();
@@ -682,11 +696,10 @@ async function interactiveCommand() {
682
696
  showPath();
683
697
  });
684
698
  screen.key(["q", "C-c"], () => {
685
- screen.destroy();
686
- process.exit(0);
699
+ cleanExit();
687
700
  });
688
- screen.key(["n"], async () => {
689
- await createNewWorktree();
701
+ screen.key(["n"], () => {
702
+ startCreationWizard();
690
703
  });
691
704
  screen.key(["d"], async () => {
692
705
  await deleteSelected();
@@ -697,17 +710,12 @@ async function interactiveCommand() {
697
710
  screen.key(["x"], async () => {
698
711
  await launchAI("codex");
699
712
  });
700
- screen.key(["p"], async () => {
701
- await pushSelected();
702
- });
703
713
  screen.key(["enter"], () => {
704
714
  const wt = worktrees[selectedIndex];
705
715
  if (wt) {
706
- screen.destroy();
707
- console.log(`
716
+ cleanExit(`
708
717
  cd "${wt.path}"
709
718
  `);
710
- process.exit(0);
711
719
  }
712
720
  });
713
721
  screen.key(["r"], async () => {
@@ -743,54 +751,364 @@ function setStatus(msg) {
743
751
  statusBar.setContent(` ${msg}`);
744
752
  screen.render();
745
753
  }
746
- async function createNewWorktree() {
747
- const input = blessed.textbox({
754
+ function cleanExit(message) {
755
+ screen.program.clear();
756
+ screen.program.disableMouse();
757
+ screen.program.showCursor();
758
+ screen.program.normalBuffer();
759
+ screen.destroy();
760
+ if (message) {
761
+ console.log(message);
762
+ }
763
+ process.exit(0);
764
+ }
765
+ function startCreationWizard() {
766
+ const state = {
767
+ branchName: "",
768
+ baseBranch: "current",
769
+ copyEnv: true,
770
+ pushToRemote: false,
771
+ aiTool: "claude"
772
+ };
773
+ askBranchName(state);
774
+ }
775
+ function askBranchName(state) {
776
+ const form = blessed.box({
748
777
  parent: screen,
749
778
  top: "center",
750
779
  left: "center",
751
- width: 50,
752
- height: 3,
780
+ width: 60,
781
+ height: 12,
753
782
  border: { type: "line" },
754
783
  style: {
755
- fg: "white",
756
- bg: "black",
757
- border: { fg: "blue" }
784
+ fg: "default",
785
+ border: { fg: "cyan" }
786
+ },
787
+ label: " New Worktree "
788
+ });
789
+ blessed.text({
790
+ parent: form,
791
+ top: 1,
792
+ left: 2,
793
+ content: `Repository: ${path8.basename(mainRepoPath)}`,
794
+ style: { fg: "cyan" }
795
+ });
796
+ blessed.text({
797
+ parent: form,
798
+ top: 2,
799
+ left: 2,
800
+ content: `Current branch: ${currentBranch}`,
801
+ style: { fg: "default" }
802
+ });
803
+ blessed.text({
804
+ parent: form,
805
+ top: 4,
806
+ left: 2,
807
+ content: "Branch name:",
808
+ style: { fg: "default" }
809
+ });
810
+ const input = blessed.textbox({
811
+ parent: form,
812
+ top: 5,
813
+ left: 2,
814
+ width: 54,
815
+ height: 1,
816
+ style: {
817
+ fg: "black",
818
+ bg: "white"
758
819
  },
759
- label: " Branch name ",
760
820
  inputOnFocus: true
761
821
  });
822
+ blessed.text({
823
+ parent: form,
824
+ top: 7,
825
+ left: 2,
826
+ content: "[Enter] next [Esc] cancel",
827
+ style: { fg: "cyan" }
828
+ });
762
829
  input.focus();
763
830
  screen.render();
764
- input.on("submit", async (value) => {
765
- input.destroy();
831
+ input.on("submit", (value) => {
766
832
  if (!value || !value.trim()) {
767
- await refreshWorktrees();
833
+ form.destroy();
834
+ screen.render();
768
835
  return;
769
836
  }
770
- const branchName = value.trim();
771
837
  try {
772
- validateBranchName(branchName);
773
- } catch (e) {
774
- setStatus(`Error: ${e.message}`);
775
- return;
776
- }
777
- setStatus(`Creating ${branchName}...`);
778
- try {
779
- const worktreePath = getWorktreePath(mainRepoPath, branchName);
780
- await createWorktree(worktreePath, branchName);
781
- await copyEnvFiles(mainRepoPath, worktreePath);
782
- setStatus(`Created ${branchName}`);
783
- await refreshWorktrees();
838
+ validateBranchName(value.trim());
839
+ state.branchName = value.trim();
840
+ form.destroy();
841
+ askBaseBranch(state);
784
842
  } catch (e) {
785
843
  setStatus(`Error: ${e.message}`);
844
+ input.focus();
845
+ screen.render();
786
846
  }
787
847
  });
788
848
  input.on("cancel", () => {
789
- input.destroy();
790
- refreshWorktrees();
849
+ form.destroy();
850
+ screen.render();
791
851
  });
792
852
  input.readInput();
793
853
  }
854
+ function askBaseBranch(state) {
855
+ const form = blessed.box({
856
+ parent: screen,
857
+ top: "center",
858
+ left: "center",
859
+ width: 50,
860
+ height: 10,
861
+ border: { type: "line" },
862
+ style: {
863
+ fg: "default",
864
+ border: { fg: "cyan" }
865
+ },
866
+ label: " Base Branch "
867
+ });
868
+ blessed.text({
869
+ parent: form,
870
+ top: 1,
871
+ left: 2,
872
+ content: "Create worktree from:",
873
+ style: { fg: "default" }
874
+ });
875
+ const list = blessed.list({
876
+ parent: form,
877
+ top: 3,
878
+ left: 2,
879
+ width: 44,
880
+ height: 3,
881
+ keys: true,
882
+ vi: true,
883
+ style: {
884
+ selected: { bg: "cyan", fg: "black" },
885
+ item: { fg: "default" }
886
+ },
887
+ items: [
888
+ ` Current branch (${currentBranch})`,
889
+ ` Default branch (${defaultBranch})`
890
+ ]
891
+ });
892
+ blessed.text({
893
+ parent: form,
894
+ top: 7,
895
+ left: 2,
896
+ content: "[Enter] select [Esc] cancel",
897
+ style: { fg: "cyan" }
898
+ });
899
+ list.focus();
900
+ screen.render();
901
+ list.on("select", (_item, index) => {
902
+ state.baseBranch = index === 0 ? "current" : "default";
903
+ form.destroy();
904
+ askCopyEnv(state);
905
+ });
906
+ list.key(["escape"], () => {
907
+ form.destroy();
908
+ screen.render();
909
+ });
910
+ }
911
+ function askCopyEnv(state) {
912
+ const form = blessed.box({
913
+ parent: screen,
914
+ top: "center",
915
+ left: "center",
916
+ width: 40,
917
+ height: 8,
918
+ border: { type: "line" },
919
+ style: {
920
+ fg: "default",
921
+ border: { fg: "cyan" }
922
+ },
923
+ label: " Environment Files "
924
+ });
925
+ blessed.text({
926
+ parent: form,
927
+ top: 1,
928
+ left: 2,
929
+ content: "Copy .env files to worktree?",
930
+ style: { fg: "default" }
931
+ });
932
+ const list = blessed.list({
933
+ parent: form,
934
+ top: 3,
935
+ left: 2,
936
+ width: 34,
937
+ height: 2,
938
+ keys: true,
939
+ vi: true,
940
+ style: {
941
+ selected: { bg: "cyan", fg: "black" },
942
+ item: { fg: "default" }
943
+ },
944
+ items: [" Yes (recommended)", " No"]
945
+ });
946
+ list.focus();
947
+ screen.render();
948
+ list.on("select", (_item, index) => {
949
+ state.copyEnv = index === 0;
950
+ form.destroy();
951
+ askPushToRemote(state);
952
+ });
953
+ list.key(["escape"], () => {
954
+ form.destroy();
955
+ screen.render();
956
+ });
957
+ }
958
+ function askPushToRemote(state) {
959
+ const form = blessed.box({
960
+ parent: screen,
961
+ top: "center",
962
+ left: "center",
963
+ width: 45,
964
+ height: 8,
965
+ border: { type: "line" },
966
+ style: {
967
+ fg: "default",
968
+ border: { fg: "cyan" }
969
+ },
970
+ label: " Push to Remote "
971
+ });
972
+ blessed.text({
973
+ parent: form,
974
+ top: 1,
975
+ left: 2,
976
+ content: "Push branch to GitHub immediately?",
977
+ style: { fg: "default" }
978
+ });
979
+ const list = blessed.list({
980
+ parent: form,
981
+ top: 3,
982
+ left: 2,
983
+ width: 39,
984
+ height: 2,
985
+ keys: true,
986
+ vi: true,
987
+ style: {
988
+ selected: { bg: "cyan", fg: "black" },
989
+ item: { fg: "default" }
990
+ },
991
+ items: [" No (push later)", " Yes (visible on GitHub now)"]
992
+ });
993
+ list.focus();
994
+ screen.render();
995
+ list.on("select", (_item, index) => {
996
+ state.pushToRemote = index === 1;
997
+ form.destroy();
998
+ askAITool(state);
999
+ });
1000
+ list.key(["escape"], () => {
1001
+ form.destroy();
1002
+ screen.render();
1003
+ });
1004
+ }
1005
+ function askAITool(state) {
1006
+ const form = blessed.box({
1007
+ parent: screen,
1008
+ top: "center",
1009
+ left: "center",
1010
+ width: 40,
1011
+ height: 10,
1012
+ border: { type: "line" },
1013
+ style: {
1014
+ fg: "default",
1015
+ border: { fg: "cyan" }
1016
+ },
1017
+ label: " Launch AI Tool "
1018
+ });
1019
+ blessed.text({
1020
+ parent: form,
1021
+ top: 1,
1022
+ left: 2,
1023
+ content: "Which AI assistant to launch?",
1024
+ style: { fg: "default" }
1025
+ });
1026
+ const list = blessed.list({
1027
+ parent: form,
1028
+ top: 3,
1029
+ left: 2,
1030
+ width: 34,
1031
+ height: 3,
1032
+ keys: true,
1033
+ vi: true,
1034
+ style: {
1035
+ selected: { bg: "cyan", fg: "black" },
1036
+ item: { fg: "default" }
1037
+ },
1038
+ items: [" Claude Code", " Codex", " Skip (just create worktree)"]
1039
+ });
1040
+ list.focus();
1041
+ screen.render();
1042
+ list.on("select", (_item, index) => {
1043
+ state.aiTool = index === 0 ? "claude" : index === 1 ? "codex" : "skip";
1044
+ form.destroy();
1045
+ executeCreation(state);
1046
+ });
1047
+ list.key(["escape"], () => {
1048
+ form.destroy();
1049
+ screen.render();
1050
+ });
1051
+ }
1052
+ async function executeCreation(state) {
1053
+ const { branchName, baseBranch, copyEnv, pushToRemote, aiTool } = state;
1054
+ setStatus(`Creating ${branchName}...`);
1055
+ try {
1056
+ if (baseBranch === "default" && currentBranch !== defaultBranch) {
1057
+ const worktreePath = getWorktreePath(mainRepoPath, branchName);
1058
+ const { execFile: execFile2 } = await import("child_process");
1059
+ const { promisify: promisify2 } = await import("util");
1060
+ const execFileAsync2 = promisify2(execFile2);
1061
+ await execFileAsync2("git", ["worktree", "add", "-b", branchName, "--", worktreePath, defaultBranch]);
1062
+ if (copyEnv) {
1063
+ await copyEnvFiles(mainRepoPath, worktreePath);
1064
+ }
1065
+ if (pushToRemote) {
1066
+ setStatus(`Pushing ${branchName}...`);
1067
+ await pushBranch(branchName, worktreePath);
1068
+ }
1069
+ await refreshWorktrees();
1070
+ setStatus(`Created ${branchName}`);
1071
+ if (aiTool !== "skip") {
1072
+ await launchInWorktree(worktreePath, aiTool);
1073
+ }
1074
+ } else {
1075
+ const worktreePath = getWorktreePath(mainRepoPath, branchName);
1076
+ await createWorktree(worktreePath, branchName);
1077
+ if (copyEnv) {
1078
+ await copyEnvFiles(mainRepoPath, worktreePath);
1079
+ }
1080
+ if (pushToRemote) {
1081
+ setStatus(`Pushing ${branchName}...`);
1082
+ await pushBranch(branchName, worktreePath);
1083
+ }
1084
+ await refreshWorktrees();
1085
+ setStatus(`Created ${branchName}`);
1086
+ if (aiTool !== "skip") {
1087
+ await launchInWorktree(worktreePath, aiTool);
1088
+ }
1089
+ }
1090
+ } catch (e) {
1091
+ setStatus(`Error: ${e.message}`);
1092
+ }
1093
+ }
1094
+ async function launchInWorktree(worktreePath, tool) {
1095
+ const available = await isToolAvailable(tool);
1096
+ if (!available) {
1097
+ setStatus(`${tool} is not installed`);
1098
+ return;
1099
+ }
1100
+ setStatus(`Launching ${tool}...`);
1101
+ screen.program.clear();
1102
+ screen.program.disableMouse();
1103
+ screen.program.showCursor();
1104
+ screen.program.normalBuffer();
1105
+ screen.destroy();
1106
+ launchAITool({ cwd: worktreePath, tool });
1107
+ console.log(`
1108
+ ${tool} launched in: ${worktreePath}
1109
+ `);
1110
+ process.exit(0);
1111
+ }
794
1112
  async function deleteSelected() {
795
1113
  const wt = worktrees[selectedIndex];
796
1114
  if (!wt) return;
@@ -807,8 +1125,7 @@ async function deleteSelected() {
807
1125
  height: 5,
808
1126
  border: { type: "line" },
809
1127
  style: {
810
- fg: "white",
811
- bg: "black",
1128
+ fg: "default",
812
1129
  border: { fg: "red" }
813
1130
  }
814
1131
  });
@@ -843,27 +1160,21 @@ async function launchAI(tool) {
843
1160
  return;
844
1161
  }
845
1162
  setStatus(`Launching ${tool}...`);
1163
+ screen.program.clear();
1164
+ screen.program.disableMouse();
1165
+ screen.program.showCursor();
1166
+ screen.program.normalBuffer();
1167
+ screen.destroy();
846
1168
  launchAITool({ cwd: wt.path, tool });
847
- setStatus(`${tool} launched in ${path8.basename(wt.path)}`);
848
- }
849
- async function pushSelected() {
850
- const wt = worktrees[selectedIndex];
851
- if (!wt || !wt.branch) {
852
- setStatus("No branch to push");
853
- return;
854
- }
855
- setStatus(`Pushing ${wt.branch}...`);
856
- try {
857
- await pushBranch(wt.branch, wt.path);
858
- setStatus(`Pushed ${wt.branch} to origin`);
859
- } catch (e) {
860
- setStatus(`Error: ${e.message}`);
861
- }
1169
+ console.log(`
1170
+ ${tool} launched in: ${path8.basename(wt.path)}
1171
+ `);
1172
+ process.exit(0);
862
1173
  }
863
1174
 
864
1175
  // src/index.ts
865
1176
  var program = new Command();
866
- program.name("wt").description("CLI tool to streamline git worktrees with AI coding assistants").version("1.3.0").action(async () => {
1177
+ program.name("wt").description("CLI tool to streamline git worktrees with AI coding assistants").version("1.4.1").action(async () => {
867
1178
  await interactiveCommand();
868
1179
  });
869
1180
  program.command("new <branch-name>").description("Create a new worktree and launch AI assistant").option("-i, --install", "Run package manager install after creating worktree").option("-s, --skip-launch", "Create worktree without launching AI assistant").option("-p, --push", "Push branch to remote (visible on GitHub)").action(async (branchName, options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-launcher",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "CLI tool for managing git worktrees with AI coding assistants",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",