feat(tui): add repository picker
- add the tui command using Bubble Tea and Lip Gloss - preselect eligible repositories and disable selection for blocked repositories - add keyboard controls for navigation, selection, refresh, sync, and cancellation - require confirmation before sync and show the branch-deletion warning - show dirty paths, block reasons, and hub output for the focused repository - add TUI model tests and set the embedded version to 0.2.0
This commit is contained in:
@@ -19,7 +19,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const version = "0.1.1"
|
||||
const version = "0.2.0"
|
||||
|
||||
var defaultSkippedDirs = map[string]bool{
|
||||
".cache": true, ".claude": true, ".codex": true, ".git": true, ".next": true, ".pnpm-store": true,
|
||||
@@ -82,7 +82,7 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
fmt.Fprintf(stdout, "stack-sync %s\n", version)
|
||||
return 0
|
||||
}
|
||||
if command != "scan" && command != "sync" {
|
||||
if command != "scan" && command != "sync" && command != "tui" {
|
||||
fmt.Fprintf(stderr, "unknown command %q\n\n", command)
|
||||
usage(stderr)
|
||||
return 2
|
||||
@@ -113,6 +113,13 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
return 1
|
||||
}
|
||||
repos := inspectAll(root, paths, opts.jobs)
|
||||
if command == "tui" {
|
||||
if err := runTUI(root, repos, opts); err != nil {
|
||||
fmt.Fprintf(stderr, "run TUI: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if command == "scan" {
|
||||
if opts.json {
|
||||
@@ -184,6 +191,8 @@ func parseFlags(command string, args []string, stderr io.Writer) (options, error
|
||||
if command == "sync" {
|
||||
fs.BoolVar(&opts.yes, "yes", false, "apply without an interactive confirmation")
|
||||
fs.BoolVar(&opts.strict, "strict", false, "abort the entire run if any repository is blocked")
|
||||
}
|
||||
if command == "sync" || command == "tui" {
|
||||
fs.DurationVar(&opts.timeout, "timeout", opts.timeout, "timeout for each hub sync")
|
||||
}
|
||||
fs.Usage = func() { usage(stderr) }
|
||||
@@ -214,11 +223,13 @@ func usage(w io.Writer) {
|
||||
Usage:
|
||||
stack-sync [scan] [options]
|
||||
stack-sync sync [options]
|
||||
stack-sync tui [options]
|
||||
stack-sync version
|
||||
|
||||
Commands:
|
||||
scan Discover repositories and report whether each is safe to sync (default)
|
||||
sync Scan, confirm, re-check safety, then run hub sync in eligible repositories
|
||||
tui Interactively review, select, and sync repositories
|
||||
|
||||
Safety:
|
||||
Repositories with edits, untracked files, detached HEADs, no remotes, or an
|
||||
@@ -412,6 +423,10 @@ func gitBytes(path string, args ...string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func syncAll(root string, repos []repo, jobs int, timeout time.Duration, stdout io.Writer, quiet bool) []syncResult {
|
||||
return syncSelected(root, repos, repos, jobs, timeout, stdout, quiet)
|
||||
}
|
||||
|
||||
func syncSelected(root string, repos, workspaceRepos []repo, jobs int, timeout time.Duration, stdout io.Writer, quiet bool) []syncResult {
|
||||
type item struct {
|
||||
index int
|
||||
repo repo
|
||||
@@ -420,8 +435,8 @@ func syncAll(root string, repos []repo, jobs int, timeout time.Duration, stdout
|
||||
results := make([]syncResult, len(repos))
|
||||
var wg sync.WaitGroup
|
||||
var outputMu sync.Mutex
|
||||
allPaths := make([]string, 0, len(repos))
|
||||
for _, r := range repos {
|
||||
allPaths := make([]string, 0, len(workspaceRepos))
|
||||
for _, r := range workspaceRepos {
|
||||
allPaths = append(allPaths, r.Path)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user