cmd/sketch: separate user-visible and internal flags using flagsets

Implement comprehensive flag organization system that separates user-facing
and internal/debugging flags into distinct flagsets for improved usability.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s210c8ff84e6ed527k
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index 66c26e9..472eb8d 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -199,53 +199,90 @@
 func parseCLIFlags() CLIFlags {
 	var flags CLIFlags
 
-	flag.StringVar(&flags.addr, "addr", "localhost:0", "local debug HTTP server address")
-	flag.StringVar(&flags.skabandAddr, "skaband-addr", "https://sketch.dev", "URL of the skaband server")
-	flag.BoolVar(&flags.unsafe, "unsafe", false, "run directly without a docker container")
-	flag.BoolVar(&flags.openBrowser, "open", true, "open sketch URL in system browser; on by default except if -one-shot is used or a ssh connection is detected")
-	flag.StringVar(&flags.httprrFile, "httprr", "", "if set, record HTTP interactions to file")
-	flag.Uint64Var(&flags.maxIterations, "max-iterations", 0, "maximum number of iterations the agent should perform per turn, 0 to disable limit")
-	flag.DurationVar(&flags.maxWallTime, "max-wall-time", 0, "maximum time the agent should run per turn, 0 to disable limit")
-	flag.Float64Var(&flags.maxDollars, "max-dollars", 10.0, "maximum dollars the agent should spend per turn, 0 to disable limit")
-	flag.BoolVar(&flags.oneShot, "one-shot", false, "exit after the first turn without termui")
-	flag.StringVar(&flags.prompt, "prompt", "", "prompt to send to sketch")
-	flag.StringVar(&flags.prompt, "p", "", "prompt to send to sketch (alias for -prompt)")
-	flag.StringVar(&flags.modelName, "model", "claude", "model to use (e.g. claude, gpt4.1)")
-	flag.StringVar(&flags.llmAPIKey, "llm-api-key", "", "API key for the LLM provider; if not set, will be read from an env var")
-	flag.BoolVar(&flags.listModels, "list-models", false, "list all available models and exit")
-	flag.BoolVar(&flags.verbose, "verbose", false, "enable verbose output")
-	flag.BoolVar(&flags.version, "version", false, "print the version and exit")
-	flag.StringVar(&flags.workingDir, "C", "", "when set, change to this directory before running")
-	flag.IntVar(&flags.sshPort, "ssh_port", 0, "the host port number that the container's ssh server will listen on, or a randomly chosen port if this value is 0")
-	flag.BoolVar(&flags.forceRebuild, "force-rebuild-container", false, "rebuild Docker container")
-	flag.StringVar(&flags.initialCommit, "initial-commit", "HEAD", "the git commit reference to use as starting point (incompatible with -unsafe)")
-	flag.StringVar(&flags.dockerArgs, "docker-args", "", "additional arguments to pass to the docker create command (e.g., --memory=2g --cpus=2)")
-	flag.Var(&flags.mounts, "mount", "volume to mount in the container in format /path/on/host:/path/in/container (can be repeated)")
-	flag.BoolVar(&flags.termUI, "termui", true, "enable terminal UI")
+	// Create separate flagsets for user-visible and internal flags
+	userFlags := flag.NewFlagSet("sketch", flag.ExitOnError)
+	internalFlags := flag.NewFlagSet("sketch-internal", flag.ContinueOnError)
 
-	// Flags geared towards sketch developers or sketch internals:
-	flag.StringVar(&flags.gitUsername, "git-username", "", "(internal) username for git commits")
-	flag.StringVar(&flags.gitEmail, "git-email", "", "(internal) email for git commits")
-	flag.StringVar(&flags.sessionID, "session-id", skabandclient.NewSessionID(), "(internal) unique session-id for a sketch process")
-	flag.BoolVar(&flags.record, "httprecord", true, "(debugging) Record trace (if httprr is set)")
-	flag.BoolVar(&flags.noCleanup, "nocleanup", false, "(debugging) do not clean up docker containers on exit")
-	flag.StringVar(&flags.containerLogDest, "save-container-logs", "", "(debugging) host path to save container logs to on exit")
-	flag.StringVar(&flags.outsideHostname, "outside-hostname", "", "(internal) hostname on the outside system")
-	flag.StringVar(&flags.outsideOS, "outside-os", "", "(internal) OS on the outside system")
-	flag.StringVar(&flags.outsideWorkingDir, "outside-working-dir", "", "(internal) working dir on the outside system")
-	flag.StringVar(&flags.sketchBinaryLinux, "sketch-binary-linux", "", "(development) path to a pre-built sketch binary for linux")
-	flag.Var(&flags.experimentFlag, "x", "enable experimental features (comma-separated list or repeat flag; use 'list' to show all)")
+	// User-visible flags
+	userFlags.StringVar(&flags.addr, "addr", "localhost:0", "local HTTP server")
+	userFlags.StringVar(&flags.skabandAddr, "skaband-addr", "https://sketch.dev", "URL of the skaband server; set to empty to disable sketch.dev integration")
+	userFlags.BoolVar(&flags.unsafe, "unsafe", false, "run without a docker container")
+	userFlags.BoolVar(&flags.openBrowser, "open", true, "open sketch URL in system browser; on by default except if -one-shot is used or a ssh connection is detected")
+	userFlags.Uint64Var(&flags.maxIterations, "max-iterations", 0, "maximum number of iterations the agent should perform per turn, 0 to disable limit")
+	userFlags.DurationVar(&flags.maxWallTime, "max-wall-time", 0, "maximum time the agent should run per turn, 0 to disable limit")
+	userFlags.Float64Var(&flags.maxDollars, "max-dollars", 10.0, "maximum dollars the agent should spend per turn, 0 to disable limit")
+	userFlags.BoolVar(&flags.oneShot, "one-shot", false, "exit after the first turn without termui")
+	userFlags.StringVar(&flags.prompt, "prompt", "", "prompt to send to sketch")
+	userFlags.StringVar(&flags.prompt, "p", "", "prompt to send to sketch (alias for -prompt)")
+	userFlags.StringVar(&flags.modelName, "model", "claude", "model to use (e.g. claude, gpt4.1)")
+	userFlags.StringVar(&flags.llmAPIKey, "llm-api-key", "", "API key for the LLM provider; if not set, will be read from an env var")
+	userFlags.BoolVar(&flags.listModels, "list-models", false, "list all available models and exit")
+	userFlags.BoolVar(&flags.verbose, "verbose", false, "enable verbose output")
+	userFlags.BoolVar(&flags.version, "version", false, "print the version and exit")
+	userFlags.IntVar(&flags.sshPort, "ssh-port", 0, "the host port number that the container's ssh server will listen on, or a randomly chosen port if this value is 0")
+	userFlags.BoolVar(&flags.forceRebuild, "force-rebuild-container", false, "rebuild Docker container")
+	userFlags.StringVar(&flags.initialCommit, "initial-commit", "HEAD", "the git commit reference to use as starting point (incompatible with -unsafe)")
+	userFlags.StringVar(&flags.dockerArgs, "docker-args", "", "additional arguments to pass to the docker create command (e.g., --memory=2g --cpus=2)")
+	userFlags.Var(&flags.mounts, "mount", "volume to mount in the container in format /path/on/host:/path/in/container (can be repeated)")
+	userFlags.BoolVar(&flags.termUI, "termui", true, "enable terminal UI")
 
-	flag.StringVar(&flags.gitRemoteURL, "git-remote-url", "", "(internal) git remote for outside sketch")
-	flag.StringVar(&flags.commit, "commit", "", "(internal) the git commit reference to check out from git remote url")
-	flag.StringVar(&flags.outsideHTTP, "outside-http", "", "(internal) host for outside sketch")
+	// Internal flags (for sketch developers or internal use)
+	// Args to sketch innie:
+	internalFlags.StringVar(&flags.gitUsername, "git-username", "", "(internal) username for git commits")
+	internalFlags.StringVar(&flags.gitEmail, "git-email", "", "(internal) email for git commits")
+	internalFlags.StringVar(&flags.sessionID, "session-id", skabandclient.NewSessionID(), "(internal) unique session-id for a sketch process")
+	internalFlags.BoolVar(&flags.record, "httprecord", true, "(debugging) Record trace (if httprr is set)")
+	internalFlags.BoolVar(&flags.noCleanup, "nocleanup", false, "(debugging) do not clean up docker containers on exit")
+	internalFlags.StringVar(&flags.containerLogDest, "save-container-logs", "", "(debugging) host path to save container logs to on exit")
+	internalFlags.StringVar(&flags.outsideHostname, "outside-hostname", "", "(internal) hostname on the outside system")
+	internalFlags.StringVar(&flags.outsideOS, "outside-os", "", "(internal) OS on the outside system")
+	internalFlags.StringVar(&flags.outsideWorkingDir, "outside-working-dir", "", "(internal) working dir on the outside system")
+	internalFlags.StringVar(&flags.sketchBinaryLinux, "sketch-binary-linux", "", "(development) path to a pre-built sketch binary for linux")
+	internalFlags.StringVar(&flags.gitRemoteURL, "git-remote-url", "", "(internal) git remote for outside sketch")
+	internalFlags.StringVar(&flags.commit, "commit", "", "(internal) the git commit reference to check out from git remote url")
+	internalFlags.StringVar(&flags.outsideHTTP, "outside-http", "", "(internal) host for outside sketch")
 
-	flag.Parse()
+	// Developer flags
+	internalFlags.StringVar(&flags.httprrFile, "httprr", "", "if set, record HTTP interactions to file")
+	internalFlags.Var(&flags.experimentFlag, "x", "enable experimental features (comma-separated list or repeat flag; use 'list' to show all)")
+	// This is really only useful for someone running with "go run"
+	userFlags.StringVar(&flags.workingDir, "C", "", "when set, change to this directory before running")
+
+	// Custom usage function that shows only user-visible flags by default
+	userFlags.Usage = func() {
+		fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
+		userFlags.PrintDefaults()
+		fmt.Fprintf(os.Stderr, "\nFor additional internal/debugging flags, use -help-internal\n")
+	}
+
+	// Check if user requested internal help
+	if len(os.Args) > 1 && os.Args[1] == "-help-internal" {
+		fmt.Fprintf(os.Stderr, "Internal/debugging flags for %s:\n", os.Args[0])
+		internalFlags.PrintDefaults()
+		os.Exit(0)
+	}
+
+	// Create a combined flagset for actual parsing by merging the two flagsets
+	allFlags := flag.NewFlagSet("sketch-all", flag.ExitOnError)
+	allFlags.Usage = userFlags.Usage
+
+	// Copy all flags from userFlags to allFlags
+	userFlags.VisitAll(func(f *flag.Flag) {
+		allFlags.Var(f.Value, f.Name, f.Usage)
+	})
+
+	// Copy all flags from internalFlags to allFlags
+	internalFlags.VisitAll(func(f *flag.Flag) {
+		allFlags.Var(f.Value, f.Name, f.Usage)
+	})
+
+	// Parse all arguments with the combined flagset
+	allFlags.Parse(os.Args[1:])
 
 	// -open's default value is not a simple true/false; it depends on other flags and conditions.
 	// Distinguish between -open default value vs explicitly set.
 	openExplicit := false
-	flag.Visit(func(f *flag.Flag) {
+	allFlags.Visit(func(f *flag.Flag) {
 		if f.Name == "open" {
 			openExplicit = true
 		}