worktree-launcher 1.3.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +14 -5
  2. package/dist/index.js +349 -49
  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,18 +622,20 @@ 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
630
  title: `wt - ${repoName}`
618
631
  });
619
- blessed.box({
632
+ headerBox = blessed.box({
620
633
  parent: screen,
621
634
  top: 0,
622
635
  left: 0,
623
636
  width: "100%",
624
637
  height: 1,
625
- content: ` Worktrees: ${repoName}`,
638
+ content: ` ${repoName} (${currentBranch})`,
626
639
  style: {
627
640
  fg: "white",
628
641
  bg: "blue",
@@ -670,7 +683,7 @@ async function interactiveCommand() {
670
683
  left: 0,
671
684
  width: "100%",
672
685
  height: 1,
673
- content: " [n]ew [d]elete [c]laude [x]codex [p]ush [Enter]cd [q]uit",
686
+ content: " [n]ew [d]elete [c]laude [x]codex [Enter]cd [q]uit",
674
687
  style: {
675
688
  fg: "black",
676
689
  bg: "white"
@@ -685,8 +698,8 @@ async function interactiveCommand() {
685
698
  screen.destroy();
686
699
  process.exit(0);
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,9 +710,6 @@ 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) {
@@ -743,54 +753,354 @@ function setStatus(msg) {
743
753
  statusBar.setContent(` ${msg}`);
744
754
  screen.render();
745
755
  }
746
- async function createNewWorktree() {
747
- const input = blessed.textbox({
756
+ function startCreationWizard() {
757
+ const state = {
758
+ branchName: "",
759
+ baseBranch: "current",
760
+ copyEnv: true,
761
+ pushToRemote: false,
762
+ aiTool: "claude"
763
+ };
764
+ askBranchName(state);
765
+ }
766
+ function askBranchName(state) {
767
+ const form = blessed.box({
748
768
  parent: screen,
749
769
  top: "center",
750
770
  left: "center",
751
- width: 50,
752
- height: 3,
771
+ width: 60,
772
+ height: 12,
753
773
  border: { type: "line" },
754
774
  style: {
755
775
  fg: "white",
756
776
  bg: "black",
757
777
  border: { fg: "blue" }
758
778
  },
759
- label: " Branch name ",
779
+ label: " New Worktree "
780
+ });
781
+ blessed.text({
782
+ parent: form,
783
+ top: 1,
784
+ left: 2,
785
+ content: `Repository: ${path8.basename(mainRepoPath)}`,
786
+ style: { fg: "cyan" }
787
+ });
788
+ blessed.text({
789
+ parent: form,
790
+ top: 2,
791
+ left: 2,
792
+ content: `Current branch: ${currentBranch}`,
793
+ style: { fg: "grey" }
794
+ });
795
+ blessed.text({
796
+ parent: form,
797
+ top: 4,
798
+ left: 2,
799
+ content: "Branch name:",
800
+ style: { fg: "white" }
801
+ });
802
+ const input = blessed.textbox({
803
+ parent: form,
804
+ top: 5,
805
+ left: 2,
806
+ width: 54,
807
+ height: 1,
808
+ style: {
809
+ fg: "white",
810
+ bg: "grey"
811
+ },
760
812
  inputOnFocus: true
761
813
  });
814
+ blessed.text({
815
+ parent: form,
816
+ top: 7,
817
+ left: 2,
818
+ content: "[Enter] next [Esc] cancel",
819
+ style: { fg: "grey" }
820
+ });
762
821
  input.focus();
763
822
  screen.render();
764
- input.on("submit", async (value) => {
765
- input.destroy();
823
+ input.on("submit", (value) => {
766
824
  if (!value || !value.trim()) {
767
- await refreshWorktrees();
768
- return;
769
- }
770
- const branchName = value.trim();
771
- try {
772
- validateBranchName(branchName);
773
- } catch (e) {
774
- setStatus(`Error: ${e.message}`);
825
+ form.destroy();
826
+ screen.render();
775
827
  return;
776
828
  }
777
- setStatus(`Creating ${branchName}...`);
778
829
  try {
779
- const worktreePath = getWorktreePath(mainRepoPath, branchName);
780
- await createWorktree(worktreePath, branchName);
781
- await copyEnvFiles(mainRepoPath, worktreePath);
782
- setStatus(`Created ${branchName}`);
783
- await refreshWorktrees();
830
+ validateBranchName(value.trim());
831
+ state.branchName = value.trim();
832
+ form.destroy();
833
+ askBaseBranch(state);
784
834
  } catch (e) {
785
835
  setStatus(`Error: ${e.message}`);
836
+ input.focus();
837
+ screen.render();
786
838
  }
787
839
  });
788
840
  input.on("cancel", () => {
789
- input.destroy();
790
- refreshWorktrees();
841
+ form.destroy();
842
+ screen.render();
791
843
  });
792
844
  input.readInput();
793
845
  }
846
+ function askBaseBranch(state) {
847
+ const form = blessed.box({
848
+ parent: screen,
849
+ top: "center",
850
+ left: "center",
851
+ width: 50,
852
+ height: 10,
853
+ border: { type: "line" },
854
+ style: {
855
+ fg: "white",
856
+ bg: "black",
857
+ border: { fg: "blue" }
858
+ },
859
+ label: " Base Branch "
860
+ });
861
+ blessed.text({
862
+ parent: form,
863
+ top: 1,
864
+ left: 2,
865
+ content: "Create worktree from:",
866
+ style: { fg: "white" }
867
+ });
868
+ const list = blessed.list({
869
+ parent: form,
870
+ top: 3,
871
+ left: 2,
872
+ width: 44,
873
+ height: 3,
874
+ keys: true,
875
+ vi: true,
876
+ style: {
877
+ selected: { bg: "blue", fg: "white" },
878
+ item: { fg: "white" }
879
+ },
880
+ items: [
881
+ ` Current branch (${currentBranch})`,
882
+ ` Default branch (${defaultBranch})`
883
+ ]
884
+ });
885
+ blessed.text({
886
+ parent: form,
887
+ top: 7,
888
+ left: 2,
889
+ content: "[Enter] select [Esc] cancel",
890
+ style: { fg: "grey" }
891
+ });
892
+ list.focus();
893
+ screen.render();
894
+ list.on("select", (_item, index) => {
895
+ state.baseBranch = index === 0 ? "current" : "default";
896
+ form.destroy();
897
+ askCopyEnv(state);
898
+ });
899
+ list.key(["escape"], () => {
900
+ form.destroy();
901
+ screen.render();
902
+ });
903
+ }
904
+ function askCopyEnv(state) {
905
+ const form = blessed.box({
906
+ parent: screen,
907
+ top: "center",
908
+ left: "center",
909
+ width: 40,
910
+ height: 8,
911
+ border: { type: "line" },
912
+ style: {
913
+ fg: "white",
914
+ bg: "black",
915
+ border: { fg: "blue" }
916
+ },
917
+ label: " Environment Files "
918
+ });
919
+ blessed.text({
920
+ parent: form,
921
+ top: 1,
922
+ left: 2,
923
+ content: "Copy .env files to worktree?",
924
+ style: { fg: "white" }
925
+ });
926
+ const list = blessed.list({
927
+ parent: form,
928
+ top: 3,
929
+ left: 2,
930
+ width: 34,
931
+ height: 2,
932
+ keys: true,
933
+ vi: true,
934
+ style: {
935
+ selected: { bg: "blue", fg: "white" },
936
+ item: { fg: "white" }
937
+ },
938
+ items: [" Yes (recommended)", " No"]
939
+ });
940
+ list.focus();
941
+ screen.render();
942
+ list.on("select", (_item, index) => {
943
+ state.copyEnv = index === 0;
944
+ form.destroy();
945
+ askPushToRemote(state);
946
+ });
947
+ list.key(["escape"], () => {
948
+ form.destroy();
949
+ screen.render();
950
+ });
951
+ }
952
+ function askPushToRemote(state) {
953
+ const form = blessed.box({
954
+ parent: screen,
955
+ top: "center",
956
+ left: "center",
957
+ width: 45,
958
+ height: 8,
959
+ border: { type: "line" },
960
+ style: {
961
+ fg: "white",
962
+ bg: "black",
963
+ border: { fg: "blue" }
964
+ },
965
+ label: " Push to Remote "
966
+ });
967
+ blessed.text({
968
+ parent: form,
969
+ top: 1,
970
+ left: 2,
971
+ content: "Push branch to GitHub immediately?",
972
+ style: { fg: "white" }
973
+ });
974
+ const list = blessed.list({
975
+ parent: form,
976
+ top: 3,
977
+ left: 2,
978
+ width: 39,
979
+ height: 2,
980
+ keys: true,
981
+ vi: true,
982
+ style: {
983
+ selected: { bg: "blue", fg: "white" },
984
+ item: { fg: "white" }
985
+ },
986
+ items: [" No (push later)", " Yes (visible on GitHub now)"]
987
+ });
988
+ list.focus();
989
+ screen.render();
990
+ list.on("select", (_item, index) => {
991
+ state.pushToRemote = index === 1;
992
+ form.destroy();
993
+ askAITool(state);
994
+ });
995
+ list.key(["escape"], () => {
996
+ form.destroy();
997
+ screen.render();
998
+ });
999
+ }
1000
+ function askAITool(state) {
1001
+ const form = blessed.box({
1002
+ parent: screen,
1003
+ top: "center",
1004
+ left: "center",
1005
+ width: 40,
1006
+ height: 10,
1007
+ border: { type: "line" },
1008
+ style: {
1009
+ fg: "white",
1010
+ bg: "black",
1011
+ border: { fg: "blue" }
1012
+ },
1013
+ label: " Launch AI Tool "
1014
+ });
1015
+ blessed.text({
1016
+ parent: form,
1017
+ top: 1,
1018
+ left: 2,
1019
+ content: "Which AI assistant to launch?",
1020
+ style: { fg: "white" }
1021
+ });
1022
+ const list = blessed.list({
1023
+ parent: form,
1024
+ top: 3,
1025
+ left: 2,
1026
+ width: 34,
1027
+ height: 3,
1028
+ keys: true,
1029
+ vi: true,
1030
+ style: {
1031
+ selected: { bg: "blue", fg: "white" },
1032
+ item: { fg: "white" }
1033
+ },
1034
+ items: [" Claude Code", " Codex", " Skip (just create worktree)"]
1035
+ });
1036
+ list.focus();
1037
+ screen.render();
1038
+ list.on("select", (_item, index) => {
1039
+ state.aiTool = index === 0 ? "claude" : index === 1 ? "codex" : "skip";
1040
+ form.destroy();
1041
+ executeCreation(state);
1042
+ });
1043
+ list.key(["escape"], () => {
1044
+ form.destroy();
1045
+ screen.render();
1046
+ });
1047
+ }
1048
+ async function executeCreation(state) {
1049
+ const { branchName, baseBranch, copyEnv, pushToRemote, aiTool } = state;
1050
+ setStatus(`Creating ${branchName}...`);
1051
+ try {
1052
+ if (baseBranch === "default" && currentBranch !== defaultBranch) {
1053
+ const worktreePath = getWorktreePath(mainRepoPath, branchName);
1054
+ const { execFile: execFile2 } = await import("child_process");
1055
+ const { promisify: promisify2 } = await import("util");
1056
+ const execFileAsync2 = promisify2(execFile2);
1057
+ await execFileAsync2("git", ["worktree", "add", "-b", branchName, "--", worktreePath, defaultBranch]);
1058
+ if (copyEnv) {
1059
+ await copyEnvFiles(mainRepoPath, worktreePath);
1060
+ }
1061
+ if (pushToRemote) {
1062
+ setStatus(`Pushing ${branchName}...`);
1063
+ await pushBranch(branchName, worktreePath);
1064
+ }
1065
+ await refreshWorktrees();
1066
+ setStatus(`Created ${branchName}`);
1067
+ if (aiTool !== "skip") {
1068
+ await launchInWorktree(worktreePath, aiTool);
1069
+ }
1070
+ } else {
1071
+ const worktreePath = getWorktreePath(mainRepoPath, branchName);
1072
+ await createWorktree(worktreePath, branchName);
1073
+ if (copyEnv) {
1074
+ await copyEnvFiles(mainRepoPath, worktreePath);
1075
+ }
1076
+ if (pushToRemote) {
1077
+ setStatus(`Pushing ${branchName}...`);
1078
+ await pushBranch(branchName, worktreePath);
1079
+ }
1080
+ await refreshWorktrees();
1081
+ setStatus(`Created ${branchName}`);
1082
+ if (aiTool !== "skip") {
1083
+ await launchInWorktree(worktreePath, aiTool);
1084
+ }
1085
+ }
1086
+ } catch (e) {
1087
+ setStatus(`Error: ${e.message}`);
1088
+ }
1089
+ }
1090
+ async function launchInWorktree(worktreePath, tool) {
1091
+ const available = await isToolAvailable(tool);
1092
+ if (!available) {
1093
+ setStatus(`${tool} is not installed`);
1094
+ return;
1095
+ }
1096
+ setStatus(`Launching ${tool}...`);
1097
+ screen.destroy();
1098
+ launchAITool({ cwd: worktreePath, tool });
1099
+ console.log(`
1100
+ ${tool} launched in: ${worktreePath}
1101
+ `);
1102
+ process.exit(0);
1103
+ }
794
1104
  async function deleteSelected() {
795
1105
  const wt = worktrees[selectedIndex];
796
1106
  if (!wt) return;
@@ -843,27 +1153,17 @@ async function launchAI(tool) {
843
1153
  return;
844
1154
  }
845
1155
  setStatus(`Launching ${tool}...`);
1156
+ screen.destroy();
846
1157
  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
- }
1158
+ console.log(`
1159
+ ${tool} launched in: ${path8.basename(wt.path)}
1160
+ `);
1161
+ process.exit(0);
862
1162
  }
863
1163
 
864
1164
  // src/index.ts
865
1165
  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 () => {
1166
+ program.name("wt").description("CLI tool to streamline git worktrees with AI coding assistants").version("1.4.0").action(async () => {
867
1167
  await interactiveCommand();
868
1168
  });
869
1169
  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.0",
4
4
  "description": "CLI tool for managing git worktrees with AI coding assistants",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",