go-cli-help-flag-starts-server
Root cause: stdlib flag.Parse stops at the first non-flag argument. With canopyd serve --help, serve is the first non-flag arg, so parsing halts there, --help is never seen, and the router dispatches serve straight into server startup — binding a port instead of printing help.
Fix (two parts, in ~/canopyd/main.go):
-h/--help/-help before subcommand dispatch, so help wins regardless of position:for _, a := range args {
if isHelpFlag(a) {
fmt.Fprint(stdout, usageText) // static, env-only usage
return 0
}
}
serve handled as explicit alias before routing — strip the subcommand, then parse remaining flags with the serve flag set:case "serve":
// "serve" is an explicit alias of the default action.
return runServe(args[1:], stdout, stderr)
The help path prints a fully static usageText (documents CANOPYD_HOST/CANOPYD_PORT/CANOPYD_LOG) — no flag parsing, no env reads, no socket code is reached. runServe keeps a normal flag.NewFlagSet with ContinueOnError for --port/--host/--log.
`go vet` clean; `go test -v` → **10/10 PASS** (`canopyd` 0.755s). Unit tests target the pure `run()` dispatcher; integration tests build the real binary and exercise actual processes/ports: | Test | Result | |---|---| | `serve --help` exits 0, prints env usage, no "listening" text | PASS | | Help variants: `-h`, `--help`, `-help`, after subcommand, after other flags, `help` cmd | PASS | | `version` / `-v` / `--version` exit 0 | PASS | | Unknown command → exit 2 with usage on stderr | PASS | | `serve --bogus`, `serve extra-arg` → exit 2 | PASS | | **Regression:** `serve --help` on built binary → port immediately re-bindable (no leak) | PASS | | **Regression:** `serve -h` → port immediately re-bindable | PASS | | Control: `serve` (no help) → prints "listening", port actually bound | PASS | | `serve` exits 0 on SIGINT | PASS | Manual check: `CANOPYD_PORT=39999 ./canopyd serve --help` → `exit=0`, port 39999 confirmed free afterwards; `./canopyd version` → exit 0; `./canopyd nope` → exit 2. Edge cases covered: help flag anywhere on the line (before/after subcommand, mixed with other flags), both dash spellings, plus the control proving the server *does* bind when help is absent.
{"model": "deepseek-v4-flash", "problem_class": "go-cli-help-flag-starts-server", "result": "passed", "tests": 10}Root cause: stdlib flag.Parse stops at the first non-flag argument. With canopyd serve --help, serve is the first non-flag arg, so parsing halts there, --help is never seen, and the router dispatches serve straight into server startup — binding a port instead of printing help.
Fix (two parts, in ~/canopyd/main.go):
-h/--help/-help before subcommand dispatch, so help wins regardless of position:for _, a := range args {
if isHelpFlag(a) {
fmt.Fprint(stdout, usageText) // static, env-only usage
return 0
}
}
serve handled as explicit alias before routing — strip the subcommand, then parse remaining flags with the serve flag set:case "serve":
// "serve" is an explicit alias of the default action.
return runServe(args[1:], stdout, stderr)
The help path prints a fully static usageText (documents CANOPYD_HOST/CANOPYD_PORT/CANOPYD_LOG) — no flag parsing, no env reads, no socket code is reached. runServe keeps a normal flag.NewFlagSet with ContinueOnError for --port/--host/--log.
`go vet` clean; `go test -v` → **10/10 PASS** (`canopyd` 0.755s). Unit tests target the pure `run()` dispatcher; integration tests build the real binary and exercise actual processes/ports: | Test | Result | |---|---| | `serve --help` exits 0, prints env usage, no "listening" text | PASS | | Help variants: `-h`, `--help`, `-help`, after subcommand, after other flags, `help` cmd | PASS | | `version` / `-v` / `--version` exit 0 | PASS | | Unknown command → exit 2 with usage on stderr | PASS | | `serve --bogus`, `serve extra-arg` → exit 2 | PASS | | **Regression:** `serve --help` on built binary → port immediately re-bindable (no leak) | PASS | | **Regression:** `serve -h` → port immediately re-bindable | PASS | | Control: `serve` (no help) → prints "listening", port actually bound | PASS | | `serve` exits 0 on SIGINT | PASS | Manual check: `CANOPYD_PORT=39999 ./canopyd serve --help` → `exit=0`, port 39999 confirmed free afterwards; `./canopyd version` → exit 0; `./canopyd nope` → exit 2. Edge cases covered: help flag anywhere on the line (before/after subcommand, mixed with other flags), both dash spellings, plus the control proving the server *does* bind when help is absent.
{"model": "deepseek-v4-flash", "problem_class": "go-cli-help-flag-starts-server", "result": "passed", "tests": 10}