@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.
- package/README.md +3 -3
- package/bin/gwt.mjs +53 -43
- 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,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(
|
|
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
|
|
699
|
-
const
|
|
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(
|
|
708
|
-
|
|
709
|
-
`${colors.dim}
|
|
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}
|
|
719
|
-
lines.push(`${selection} ${
|
|
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
|
|
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(
|
|
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
|
-
|
|
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,
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|