feat(tui): add interactive repository selection
Introduce a Bubble Tea control deck with eligible repositories preselected, keyboard navigation, responsive paging, and detailed explanations for blocked worktrees. Require confirmation before synchronization, allow refreshing the workspace, and retain each repository's latest hub output without changing the existing CLI and JSON workflows.
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