@junheep/gwt 0.2.0 → 0.2.2
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 +3 -3
- package/bin/gwt.mjs +96 -47
- 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
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
662
|
-
const
|
|
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
|
|
665
|
+
current: current && resolve(current.path) === resolve(worktree.path),
|
|
666
|
+
id: item?.id ?? (index === 0 ? "primary" : "-"),
|
|
667
667
|
branch: worktree.branch ?? "(detached)",
|
|
668
|
-
|
|
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
|
-
|
|
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 =
|
|
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,21 @@ 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(
|
|
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
|
|
697
|
-
const
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
: value.padEnd(width)
|
|
714
|
+
const setupWidth = Math.max(5, ...filtered.map((choice) => displayWidth(choice.setup)))
|
|
715
|
+
const longestBranch = Math.max(12, ...filtered.map((choice) => displayWidth(choice.branch)))
|
|
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)
|
|
703
719
|
const visibleCount = Math.max(3, (process.stdout.rows ?? 24) - 5)
|
|
704
720
|
const start = Math.max(0, Math.min(selected - Math.floor(visibleCount / 2), filtered.length - visibleCount))
|
|
705
721
|
const visible = filtered.slice(start, start + visibleCount)
|
|
722
|
+
const escapeAction = filtering ? "clear" : "cancel"
|
|
706
723
|
const lines = [
|
|
707
|
-
`${colors.dim}${
|
|
708
|
-
|
|
709
|
-
`${colors.dim}
|
|
724
|
+
`${colors.dim}${fitDisplay(`Switch worktree Esc ${escapeAction} · Enter select · ↑↓/jk/C-n/C-p · / filter`, terminalWidth)}${colors.reset}`,
|
|
725
|
+
...(filtering ? [fitDisplay(`Filter: /${query}`, terminalWidth)] : []),
|
|
726
|
+
`${colors.dim} ${fitDisplay("BRANCH", branchWidth)} ${fitDisplay("ID", idWidth)} ${fitDisplay("SETUP", setupWidth)} ${fitDisplay("PATH", pathWidth)}${colors.reset}`,
|
|
710
727
|
]
|
|
711
728
|
|
|
712
729
|
if (visible.length === 0) {
|
|
@@ -715,8 +732,8 @@ async function chooseWorktree(repository) {
|
|
|
715
732
|
visible.forEach((choice, visibleIndex) => {
|
|
716
733
|
const index = start + visibleIndex
|
|
717
734
|
const selection = index === selected ? `${colors.cyan}>${colors.reset}` : " "
|
|
718
|
-
const currentMarker = choice.current ? `${colors.yellow}
|
|
719
|
-
lines.push(`${selection} ${
|
|
735
|
+
const currentMarker = choice.current ? `${colors.yellow}*${colors.reset}` : " "
|
|
736
|
+
lines.push(`${selection} ${currentMarker} ${fitDisplay(choice.branch, branchWidth)} ${fitDisplay(choice.id, idWidth)} ${fitDisplay(choice.setup, setupWidth)} ${fitDisplay(choice.path, pathWidth)}`)
|
|
720
737
|
})
|
|
721
738
|
}
|
|
722
739
|
|
|
@@ -747,7 +764,10 @@ async function chooseWorktree(repository) {
|
|
|
747
764
|
}
|
|
748
765
|
if (key.name === "escape") {
|
|
749
766
|
if (filtering) {
|
|
767
|
+
const selectedChoice = filtered[selected]
|
|
750
768
|
filtering = false
|
|
769
|
+
query = ""
|
|
770
|
+
selected = selectedChoice ? choices.indexOf(selectedChoice) : initialSelected
|
|
751
771
|
render()
|
|
752
772
|
} else {
|
|
753
773
|
finish(new CliError("Selection cancelled"))
|
|
@@ -768,11 +788,6 @@ async function chooseWorktree(repository) {
|
|
|
768
788
|
} else if (filtering && key.name === "backspace") {
|
|
769
789
|
query = [...query].slice(0, -1).join("")
|
|
770
790
|
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
791
|
} else if (filtering && text && !key.ctrl && !key.meta) {
|
|
777
792
|
query += text.replace(/[\x00-\x1f\x7f]/g, "")
|
|
778
793
|
selected = 0
|
|
@@ -780,7 +795,7 @@ async function chooseWorktree(repository) {
|
|
|
780
795
|
render()
|
|
781
796
|
}
|
|
782
797
|
|
|
783
|
-
emitKeypressEvents(process.stdin)
|
|
798
|
+
emitKeypressEvents(process.stdin, { escapeCodeTimeout: PICKER_ESCAPE_CODE_TIMEOUT_MS })
|
|
784
799
|
process.stdin.on("keypress", onKeypress)
|
|
785
800
|
process.stdout.on("resize", render)
|
|
786
801
|
process.stdin.setRawMode(true)
|
|
@@ -799,18 +814,8 @@ async function commandSwitch(args) {
|
|
|
799
814
|
}
|
|
800
815
|
|
|
801
816
|
function worktreeRows(repository) {
|
|
802
|
-
const current = currentWorktree(repository)
|
|
803
817
|
const metadata = loadMetadata(repository)
|
|
804
|
-
const rows = repository
|
|
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
|
-
})
|
|
818
|
+
const rows = linkedWorktreeRows(repository, metadata)
|
|
814
819
|
const registeredPaths = new Set(repository.worktrees.map((worktree) => resolve(worktree.path)))
|
|
815
820
|
for (const item of metadata.filter((entry) => !registeredPaths.has(resolve(entry.path)))) {
|
|
816
821
|
rows.push({ current: false, id: item.id, branch: "-", setup: "stale", path: item.path })
|
|
@@ -846,18 +851,62 @@ function padDisplay(value, width) {
|
|
|
846
851
|
return `${value}${" ".repeat(Math.max(0, width - displayWidth(value)))}`
|
|
847
852
|
}
|
|
848
853
|
|
|
854
|
+
function fitDisplay(value, width) {
|
|
855
|
+
const normalized = value.normalize("NFC")
|
|
856
|
+
if (displayWidth(normalized) <= width) return padDisplay(normalized, width)
|
|
857
|
+
|
|
858
|
+
const contentWidth = width - displayWidth("…")
|
|
859
|
+
let fitted = ""
|
|
860
|
+
let fittedWidth = 0
|
|
861
|
+
for (const character of normalized) {
|
|
862
|
+
const characterWidth = displayWidth(character)
|
|
863
|
+
if (fittedWidth + characterWidth > contentWidth) break
|
|
864
|
+
fitted += character
|
|
865
|
+
fittedWidth += characterWidth
|
|
866
|
+
}
|
|
867
|
+
return padDisplay(`${fitted}…`, width)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function truncateDisplayTail(value, width) {
|
|
871
|
+
const normalized = value.normalize("NFC")
|
|
872
|
+
if (displayWidth(normalized) <= width) return normalized
|
|
873
|
+
|
|
874
|
+
const contentWidth = width - displayWidth("…")
|
|
875
|
+
let fitted = ""
|
|
876
|
+
let fittedWidth = 0
|
|
877
|
+
for (const character of [...normalized].reverse()) {
|
|
878
|
+
const characterWidth = displayWidth(character)
|
|
879
|
+
if (fittedWidth + characterWidth > contentWidth) break
|
|
880
|
+
fitted = `${character}${fitted}`
|
|
881
|
+
fittedWidth += characterWidth
|
|
882
|
+
}
|
|
883
|
+
return `…${fitted}`
|
|
884
|
+
}
|
|
885
|
+
|
|
849
886
|
function commandList(args) {
|
|
850
887
|
if (args.length > 0) throw new CliError("Usage: gwt list")
|
|
851
888
|
const repository = discoverRepository()
|
|
852
889
|
const rows = worktreeRows(repository)
|
|
890
|
+
const colors = terminalColors()
|
|
853
891
|
const widths = {
|
|
854
|
-
id: Math.max(2, ...rows.map((row) => displayWidth(row.id))),
|
|
855
892
|
branch: Math.max(6, ...rows.map((row) => displayWidth(row.branch))),
|
|
893
|
+
id: Math.max(2, ...rows.map((row) => displayWidth(row.id))),
|
|
856
894
|
setup: Math.max(5, ...rows.map((row) => displayWidth(row.setup))),
|
|
857
895
|
}
|
|
858
|
-
|
|
896
|
+
let pathWidth = null
|
|
897
|
+
if (process.stdout.isTTY) {
|
|
898
|
+
const minimumWidth = 8 + 6 + widths.id + widths.setup + 4
|
|
899
|
+
const terminalWidth = Math.max(minimumWidth, process.stdout.columns || 100)
|
|
900
|
+
const flexibleWidth = terminalWidth - widths.id - widths.setup - 8
|
|
901
|
+
widths.branch = Math.min(32, widths.branch, Math.max(6, flexibleWidth - 12))
|
|
902
|
+
pathWidth = flexibleWidth - widths.branch
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
console.log(`${colors.dim} ${fitDisplay("BRANCH", widths.branch)} ${fitDisplay("ID", widths.id)} ${fitDisplay("SETUP", widths.setup)} PATH${colors.reset}`)
|
|
859
906
|
for (const row of rows) {
|
|
860
|
-
|
|
907
|
+
const currentMarker = row.current ? `${colors.yellow}*${colors.reset}` : " "
|
|
908
|
+
const path = pathWidth === null ? row.path : truncateDisplayTail(row.path, pathWidth)
|
|
909
|
+
console.log(`${currentMarker} ${fitDisplay(row.branch, widths.branch)} ${fitDisplay(row.id, widths.id)} ${fitDisplay(row.setup, widths.setup)} ${path}`)
|
|
861
910
|
}
|
|
862
911
|
}
|
|
863
912
|
|
|
@@ -1417,9 +1466,9 @@ Options:
|
|
|
1417
1466
|
-h, --help Show help for this command.
|
|
1418
1467
|
|
|
1419
1468
|
Behavior:
|
|
1420
|
-
The picker supports arrow keys, j/k, Ctrl-n/Ctrl-p,
|
|
1421
|
-
|
|
1422
|
-
|
|
1469
|
+
The picker supports arrow keys, j/k, Ctrl-n/Ctrl-p, and '/' filtering. Shell
|
|
1470
|
+
integration must be installed for gwt to change the parent shell's directory;
|
|
1471
|
+
otherwise the selected path is only printed.
|
|
1423
1472
|
|
|
1424
1473
|
Examples:
|
|
1425
1474
|
gwt switch
|