@junheep/gwt 0.2.0 → 0.2.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 +3 -3
  2. package/bin/gwt.mjs +53 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -214,9 +214,9 @@ Setup failures retain the worktree and record the failure. Retry with
214
214
  `gwt setup <id>` or remove it explicitly.
215
215
 
216
216
  Run `gwt switch` without a target to open the interactive picker. Use the
217
- arrow keys, `j`/`k`, or Ctrl-n/Ctrl-p to move; press 1–9 to select a numbered
218
- row immediately; or press `/` to filter by branch, ID, or path. Enter switches
219
- to the selected worktree. Escape leaves filter mode or cancels the picker.
217
+ arrow keys, `j`/`k`, or Ctrl-n/Ctrl-p to move; press `/` to filter by branch,
218
+ ID, or path. Enter switches to the selected worktree. Escape leaves filter
219
+ mode or cancels the picker.
220
220
 
221
221
  `gwt remove` refuses dirty worktrees and first tries to delete the branch with
222
222
  `git branch -d`. If Git rejects safe deletion, an interactive terminal asks
package/bin/gwt.mjs CHANGED
@@ -26,6 +26,7 @@ import { createInterface } from "node:readline/promises"
26
26
  const PROJECT_CONFIG_FILE = ".gwt.json"
27
27
  const PORT_MIN = 20_000
28
28
  const PORT_MAX = 39_999
29
+ const PICKER_ESCAPE_CODE_TIMEOUT_MS = 50
29
30
  const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
30
31
  const DEFAULT_CONFIG = { worktreeDirectory: ".worktrees", copyFiles: [], ports: [] }
31
32
  const SKILL_DIRECTORIES = { claude: ".claude", codex: ".agents" }
@@ -655,17 +656,35 @@ async function commandNew(args) {
655
656
  }
656
657
  }
657
658
 
658
- async function chooseWorktree(repository) {
659
- if (!process.stdin.isTTY || !process.stdout.isTTY) throw new CliError("A worktree selector is required in non-interactive mode")
659
+ function linkedWorktreeRows(repository, metadata = loadMetadata(repository)) {
660
660
  const current = currentWorktree(repository)
661
- const choices = repository.worktrees.map((worktree, index) => {
662
- const metadata = metadataForWorktree(repository, worktree)
663
- const relativePath = relative(repository.primaryPath, worktree.path)
661
+ return repository.worktrees.map((worktree, index) => {
662
+ const item = metadata.find((entry) => resolve(entry.path) === resolve(worktree.path))
664
663
  return {
665
664
  worktree,
666
- current: resolve(current?.path ?? "") === resolve(worktree.path),
665
+ current: current && resolve(current.path) === resolve(worktree.path),
666
+ id: item?.id ?? (index === 0 ? "primary" : "-"),
667
667
  branch: worktree.branch ?? "(detached)",
668
- id: metadata?.id ?? (index === 0 ? "primary" : "native"),
668
+ setup: item?.setup ?? (index === 0 ? "-" : "unmanaged"),
669
+ path: worktree.path,
670
+ }
671
+ })
672
+ }
673
+
674
+ function terminalColors() {
675
+ if (!process.stdout.isTTY || process.env.NO_COLOR !== undefined) {
676
+ return { cyan: "", yellow: "", dim: "", reset: "" }
677
+ }
678
+ return { cyan: "\x1b[36m", yellow: "\x1b[33m", dim: "\x1b[2m", reset: "\x1b[0m" }
679
+ }
680
+
681
+ async function chooseWorktree(repository) {
682
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new CliError("A worktree selector is required in non-interactive mode")
683
+ const choices = linkedWorktreeRows(repository).map((row) => {
684
+ const { worktree } = row
685
+ const relativePath = relative(repository.primaryPath, worktree.path)
686
+ return {
687
+ ...row,
669
688
  path: relativePath === "" ? "." : relativePath.startsWith(`..${sep}`) ? worktree.path : relativePath,
670
689
  }
671
690
  })
@@ -673,12 +692,11 @@ async function chooseWorktree(repository) {
673
692
  return new Promise((resolveChoice, rejectChoice) => {
674
693
  let query = ""
675
694
  let filtering = false
676
- let selected = Math.max(0, choices.findIndex((choice) => choice.current))
695
+ const initialSelected = Math.max(0, choices.findIndex((choice) => choice.current))
696
+ let selected = initialSelected
677
697
  let renderedLines = 0
678
698
  const wasRaw = process.stdin.isRaw
679
- const colors = process.env.NO_COLOR === undefined
680
- ? { cyan: "\x1b[36m", yellow: "\x1b[33m", dim: "\x1b[2m", reset: "\x1b[0m" }
681
- : { cyan: "", yellow: "", dim: "", reset: "" }
699
+ const colors = terminalColors()
682
700
 
683
701
  const clear = () => {
684
702
  if (renderedLines > 0) process.stdout.write(`\x1b[${renderedLines}A\r\x1b[J`)
@@ -691,22 +709,24 @@ async function chooseWorktree(repository) {
691
709
  .some((value) => value.toLowerCase().includes(normalizedQuery)))
692
710
  if (selected >= filtered.length) selected = Math.max(0, filtered.length - 1)
693
711
 
694
- const terminalWidth = Math.max(40, process.stdout.columns ?? 100)
695
- const numberWidth = String(Math.max(1, filtered.length)).length
712
+ const terminalWidth = Math.max(48, process.stdout.columns ?? 100)
696
713
  const idWidth = 8
714
+ const setupWidth = Math.max(5, ...filtered.map((choice) => choice.setup.length))
697
715
  const longestBranch = Math.max(12, ...filtered.map((choice) => choice.branch.length))
698
- const branchWidth = Math.min(32, longestBranch, terminalWidth - numberWidth - idWidth - 22)
699
- const pathWidth = Math.max(8, terminalWidth - numberWidth - branchWidth - idWidth - 10)
716
+ const flexibleWidth = terminalWidth - idWidth - setupWidth - 10
717
+ const branchWidth = Math.min(32, longestBranch, Math.max(8, flexibleWidth - 8))
718
+ const pathWidth = Math.max(8, flexibleWidth - branchWidth)
700
719
  const fit = (value, width) => value.length > width
701
720
  ? `${value.slice(0, Math.max(0, width - 1))}…`
702
721
  : value.padEnd(width)
703
722
  const visibleCount = Math.max(3, (process.stdout.rows ?? 24) - 5)
704
723
  const start = Math.max(0, Math.min(selected - Math.floor(visibleCount / 2), filtered.length - visibleCount))
705
724
  const visible = filtered.slice(start, start + visibleCount)
725
+ const escapeAction = filtering ? "clear" : "cancel"
706
726
  const lines = [
707
- `${colors.dim}${fit("Switch worktree ↑↓/jk/C-n/C-p move · 1-9 select · / filter · Enter", terminalWidth)}${colors.reset}`,
708
- fit(`Filter: ${filtering ? "/" : ""}${query}`, terminalWidth),
709
- `${colors.dim} ${fit("#", numberWidth)} ${fit("BRANCH", branchWidth)} ${fit("ID", idWidth)} ${fit("PATH", pathWidth)}${colors.reset}`,
727
+ `${colors.dim}${fit(`Switch worktree Esc ${escapeAction} · Enter select · ↑↓/jk/C-n/C-p · / filter`, terminalWidth)}${colors.reset}`,
728
+ ...(filtering ? [fit(`Filter: /${query}`, terminalWidth)] : []),
729
+ `${colors.dim} ${fit("BRANCH", branchWidth)} ${fit("ID", idWidth)} ${fit("SETUP", setupWidth)} ${fit("PATH", pathWidth)}${colors.reset}`,
710
730
  ]
711
731
 
712
732
  if (visible.length === 0) {
@@ -715,8 +735,8 @@ async function chooseWorktree(repository) {
715
735
  visible.forEach((choice, visibleIndex) => {
716
736
  const index = start + visibleIndex
717
737
  const selection = index === selected ? `${colors.cyan}>${colors.reset}` : " "
718
- const currentMarker = choice.current ? `${colors.yellow}@${colors.reset}` : " "
719
- lines.push(`${selection} ${fit(String(index + 1), numberWidth)} ${currentMarker} ${fit(choice.branch, branchWidth)} ${fit(choice.id, idWidth)} ${fit(choice.path, pathWidth)}`)
738
+ const currentMarker = choice.current ? `${colors.yellow}*${colors.reset}` : " "
739
+ lines.push(`${selection} ${currentMarker} ${fit(choice.branch, branchWidth)} ${fit(choice.id, idWidth)} ${fit(choice.setup, setupWidth)} ${fit(choice.path, pathWidth)}`)
720
740
  })
721
741
  }
722
742
 
@@ -747,7 +767,10 @@ async function chooseWorktree(repository) {
747
767
  }
748
768
  if (key.name === "escape") {
749
769
  if (filtering) {
770
+ const selectedChoice = filtered[selected]
750
771
  filtering = false
772
+ query = ""
773
+ selected = selectedChoice ? choices.indexOf(selectedChoice) : initialSelected
751
774
  render()
752
775
  } else {
753
776
  finish(new CliError("Selection cancelled"))
@@ -768,11 +791,6 @@ async function chooseWorktree(repository) {
768
791
  } else if (filtering && key.name === "backspace") {
769
792
  query = [...query].slice(0, -1).join("")
770
793
  selected = 0
771
- } else if (!filtering && /^[1-9]$/.test(text)) {
772
- const choice = filtered[Number(text) - 1]
773
- if (choice) finish(null, choice)
774
- else process.stdout.write("\x07")
775
- return
776
794
  } else if (filtering && text && !key.ctrl && !key.meta) {
777
795
  query += text.replace(/[\x00-\x1f\x7f]/g, "")
778
796
  selected = 0
@@ -780,7 +798,7 @@ async function chooseWorktree(repository) {
780
798
  render()
781
799
  }
782
800
 
783
- emitKeypressEvents(process.stdin)
801
+ emitKeypressEvents(process.stdin, { escapeCodeTimeout: PICKER_ESCAPE_CODE_TIMEOUT_MS })
784
802
  process.stdin.on("keypress", onKeypress)
785
803
  process.stdout.on("resize", render)
786
804
  process.stdin.setRawMode(true)
@@ -799,18 +817,8 @@ async function commandSwitch(args) {
799
817
  }
800
818
 
801
819
  function worktreeRows(repository) {
802
- const current = currentWorktree(repository)
803
820
  const metadata = loadMetadata(repository)
804
- const rows = repository.worktrees.map((worktree, index) => {
805
- const item = metadata.find((entry) => resolve(entry.path) === resolve(worktree.path))
806
- return {
807
- current: current && resolve(current.path) === resolve(worktree.path),
808
- id: item?.id ?? (index === 0 ? "primary" : "-"),
809
- branch: worktree.branch ?? "(detached)",
810
- setup: item?.setup ?? (index === 0 ? "-" : "unmanaged"),
811
- path: worktree.path,
812
- }
813
- })
821
+ const rows = linkedWorktreeRows(repository, metadata)
814
822
  const registeredPaths = new Set(repository.worktrees.map((worktree) => resolve(worktree.path)))
815
823
  for (const item of metadata.filter((entry) => !registeredPaths.has(resolve(entry.path)))) {
816
824
  rows.push({ current: false, id: item.id, branch: "-", setup: "stale", path: item.path })
@@ -850,14 +858,16 @@ function commandList(args) {
850
858
  if (args.length > 0) throw new CliError("Usage: gwt list")
851
859
  const repository = discoverRepository()
852
860
  const rows = worktreeRows(repository)
861
+ const colors = terminalColors()
853
862
  const widths = {
854
- id: Math.max(2, ...rows.map((row) => displayWidth(row.id))),
855
863
  branch: Math.max(6, ...rows.map((row) => displayWidth(row.branch))),
864
+ id: Math.max(2, ...rows.map((row) => displayWidth(row.id))),
856
865
  setup: Math.max(5, ...rows.map((row) => displayWidth(row.setup))),
857
866
  }
858
- console.log(` ${padDisplay("ID", widths.id)} ${padDisplay("BRANCH", widths.branch)} ${padDisplay("SETUP", widths.setup)} PATH`)
867
+ console.log(`${colors.dim} ${padDisplay("BRANCH", widths.branch)} ${padDisplay("ID", widths.id)} ${padDisplay("SETUP", widths.setup)} PATH${colors.reset}`)
859
868
  for (const row of rows) {
860
- console.log(`${row.current ? "*" : " "} ${padDisplay(row.id, widths.id)} ${padDisplay(row.branch, widths.branch)} ${padDisplay(row.setup, widths.setup)} ${row.path}`)
869
+ const currentMarker = row.current ? `${colors.yellow}*${colors.reset}` : " "
870
+ console.log(`${currentMarker} ${padDisplay(row.branch, widths.branch)} ${padDisplay(row.id, widths.id)} ${padDisplay(row.setup, widths.setup)} ${row.path}`)
861
871
  }
862
872
  }
863
873
 
@@ -1417,9 +1427,9 @@ Options:
1417
1427
  -h, --help Show help for this command.
1418
1428
 
1419
1429
  Behavior:
1420
- The picker supports arrow keys, j/k, Ctrl-n/Ctrl-p, number shortcuts, and
1421
- '/' filtering. Shell integration must be installed for gwt to change the
1422
- parent shell's directory; otherwise the selected path is only printed.
1430
+ The picker supports arrow keys, j/k, Ctrl-n/Ctrl-p, and '/' filtering. Shell
1431
+ integration must be installed for gwt to change the parent shell's directory;
1432
+ otherwise the selected path is only printed.
1423
1433
 
1424
1434
  Examples:
1425
1435
  gwt switch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junheep/gwt",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Lightweight native Git worktree workflows",
5
5
  "license": "MIT",
6
6
  "author": "Junhee Park",