can you take a look at @../cli and how we structure the initial command help, can we do the same here? like if you run `git-sync` you see only the subcommands and then do `git-sync sync --help` for th

can you take a look at @../cli and how we structure the initial command help, can we do the same here? like if you run git-sync you see only the subcommands and then do git-sync sync --help for the help there? Also running with --help or no params/option should feel less like an error
Took a look. The cli/ repo uses cobra, which gives you per-subcommand --help, a clean root listing, and friendly fallback for invalid usage essentially for free. git-sync currently uses the stdlib flag package and prints one big usage block as an error: for everything (no args, --help, unknown command, missing args all hit the same path).
Two ways to get the UX you want:
-
Migrate to cobra (matches
cli/exactly): each subcommand gets its own--helpautomatically, root with no args runscmd.Help()cleanly, and the suggestion-on-typo behavior incli/cmd/entire/main.goports over. Addsgithub.com/spf13/cobraas a dep, and refactors all sixrunXfunctions into*cobra.Commandconstructors. ~moderate-size change, but mostly mechanical. -
Stay with
flag, fix the UX: split the giantusageErrorblock into per-subcommand usage; when args are empty or--help/-his passed, print a brief root listing (subcommands + one-line descriptions) to stdout with exit 0 instead of stderr+exit 1; only printerror:for actually-bad usage. Smaller diff, no new dep, but you'll hand-roll what cobra gives you.
My recommendation is option 1 — cli/ already proves the pattern works, and given you have six subcommands with overlapping flag sets, cobra's persistent flags would also let you DRY up the source/target/auth flag declarations that are currently copy-pasted across runSyncLike, runBootstrap, runProbe, runFetch. Want me to go ahead with the cobra migration, or would you prefer the lighter-touch option 2?
Done. All cmd/git-sync/... and full-module tests pass, go vet is clean.
What changed:
cmd/git-sync/main.go— thin entry;run()builds a cobra root, executes, and prints usage instead of a one-line error on unknown command/flag.cmd/git-sync/root.go—newRootCmd(); runscmd.Help()when invoked with no args, sogit-syncandgit-sync --helpboth produce the friendly subcommand listing on stdout (exit 0).cmd/git-sync/syncplan.go—sync,replicate,plan(shared constructor).cmd/git-sync/bootstrap.go,probe.go,fetch.go,version.go— one file per command.cmd/git-sync/flags.go— shared helpers (addSourceEndpoint,addTargetEndpoint,addSourceAuth,addTargetAuth,addProtocolFlag) plus the customprotocolModeFlag/operationModeFlagtypes (now withType()for pflag).
Behavior notes:
git-sync/git-sync --help→ root help on stdout, exit 0.git-sync sync --help→ flags + usage just forsync.git-sync foo→ prints root usage +Error: unknown command "foo", exit 1 (thecli/repo's pattern).--forceis still declared onreplicateso the syncer-levelreplicate does not support --forceerror surfaces (matching the existing test).-vis preserved as the shorthand for--verboseon sync/replicate/plan/bootstrap.git-sync versionandgit-sync --versionboth work.--map,--have,--have-refuseStringArrayVar(no comma-splitting) to match the previousmultiStringFlag.--branchstill takes the comma-separated string (splitCSVpreserved).
go.mod gained github.com/spf13/cobra v1.10.2 (and pflag/mousetrap as transitive).
Mostly yes, but I found one regression and a couple of edge cases worth flagging.
Fixed just now: git-sync --version had stopped working (only git-sync version did). Wired cmd.Version + SetVersionTemplate so both forms print the same string as before.
Fully preserved: every long flag name, default, and value format; every subcommand name; positional <source-url> <target-url> handling; all GITSYNC_* env vars; JSON and text output formats; the -v shorthand; the inner error strings (e.g. replicate does not support --force).
Intentionally improved (what you asked for): git-sync, git-sync --help, and git-sync <cmd> --help now print clean help to stdout with exit 0 instead of going through the usage-as-error path.
Edge-case differences from the flag → pflag switch — the only places where someone's existing invocation could behave differently:
- Single-dash long flags no longer work. stdlib
flagaccepted-source-url=fooand--source-url=foointerchangeably;pflagis POSIX, so single-dash is reserved for short flags. Anyone scriptinggit-sync sync -source-url=...has to switch to--source-url=.... Same convention ascli/. --v(long) is gone. Was implicitly valid via stdlib flag's loose name matching.-vand--verboseboth work;--vdoes not. Realistically nobody was typing--v.- Help/error text formatting changes. Same flags, rendered in cobra's columnar style instead of the hand-written block; unknown-command message reads
Error: unknown command "foo" for "git-sync"instead ofunknown command "foo". Visible only to anyone parsing help output.
If 1 or 2 matter for an existing script or doc, let me know and I can add aliases.
No. Every example in README.md, docs/usage.md, and CHANGELOG.md already uses --double-dash flags (--source-token, --branch, --map, --protocol, -v, etc.). I didn't find a single occurrence of single-dash long flags (-source-url, -branch, …) anywhere in the repo — not in markdown, not in shell scripts, not in mise tasks, not in .goreleaser.yaml. So in practice the flag → pflag switch breaks zero documented or in-tree usage.
The only theoretical break would be an external script someone wrote that used -source-url style. If you want belt-and-suspenders, I can keep the flag parser tolerant by registering double-named aliases, but I'd say it's not worth it — cli/ doesn't and our docs don't.
can you take a look:
• 1. Medium: bare git-sync now exits successfully instead of failing for missing command. In the old entrypoint, run() returned a usage error when len(args) == 0, so the process exited with status 1. After the Cobra refactor, the root command’s RunE just calls cmd.Help() and returns nil, and main() treats that as success. That is a user-visible contract change for scripts that validate invocation or expect misuse to fail. See cmd/git-sync/root.go:26 and cmd/git-sync/main.go:27. 2. Low: unknown subcommand flags now print the root usage block instead of the subcommand usage, which makes the new error-handling path materially less helpful than intended. run() always calls showUsage(cmd, err) on the root command object after ExecuteContext fails, so git-sync sync --bogus shows top-level usage rather than sync’s flags. I verified that behavior locally. See cmd/git-sync/main.go:35 and cmd/git-sync/main.go:46.
not sure about 1
Fixed and tests pass.
On (1): push back. You explicitly asked for "no params should feel less like an error." Exit 0 is what kubectl, docker, cli/ (entire CLI), and most modern multi-command CLIs do. The reviewer's "scripts validating invocation by exit code" concern is theoretical — that pattern wasn't useful before either, since the same script would fail every time someone runs git-sync --help to discover commands. Keep it as is.
On (2): real bug, fixed in cmd/git-sync/main.go:38-40. After ExecuteContext fails, we now call rootCmd.Find(args) to pick the deepest matched subcommand and print its usage. Verified:
git-sync sync --bogus→ prints sync's flag list +Error: unknown flag: --bogusgit-sync fooble→ still prints root usage +Error: unknown command "fooble"(Find returns root for unknown top-level commands)- All tests still pass.
[lint:go] golangci-lint has version 2.11.4 built with go1.26.1 from 8f3b0c7e on 2026-03-22T17:35:14Z [lint:shellcheck] Finished in 239.4ms [lint:go] cmd/git-sync/main.go:45:9: error returned from external package is unwrapped: sig: func (*github.com/spf13/cobra.Command).ExecuteContext(ctx context.Context) error (wrapcheck) [lint:go] return err [lint:go] ^ [lint:go] 1 issues: [lint:go] * wrapcheck: 1