go-cli-bare-invocation-panic
Root cause. main() defaulted cmd to "run" and then unconditionally evaluated os.Args[2:]. On a bare invocation os.Args has length 1, so os.Args[2:] computes high=2, low-cap=1 → panic: runtime error: slice bounds out of range [2:1].
Buggy code (before):
func main() {
cmd := "run" // default
if len(os.Args) > 1 {
cmd = os.Args[1]
}
switch cmd {
case "run":
run(os.Args[2:]) // panic when os.Args has length 1
case "help", "-h", "--help":
usage()
default:
usage()
}
}
Fix 1 — guard bare invocation (Go): check len(os.Args) < 2 before touching os.Args[2:], slice the args once, and print usage with a non-zero exit:
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
cmd := os.Args[1]
args := os.Args[2:] // safe: len(os.Args) >= 2
switch cmd {
case "run":
run(args)
case "help", "-h", "--help":
usage()
default:
fmt.Fprintf(os.Stderr, "dagger: unknown command %q\n\n", cmd)
usage()
os.Exit(2)
}
}
Fix 2 — correct the help wording: the first line previously claimed bare invocation resumes incomplete runs. That was false — bare dagger (and dagger help) only prints help. The wording now states the truth:
Usage: dagger <command> [options]
Commands:
run Execute a command inside the Dagger engine
help Show this help
Bare invocation with no arguments prints this help.
Fix 3 — templates run subcommand via params injection (JS): adding the subcommand by emitting a const run = {...} into the template file caused SyntaxError: Identifier 'run' has already been declared (the template scope already had that binding). The executor now injects a params global, and the template mutates it with Object.assign, which never redeclares a binding:
// Executor-injected global; template adds subcommands onto it:
const params = globalThis.__executorInjected = { hello: "world" };
// Template body (fix): Object.assign, no new const -> no duplicate-const error.
Object.assign(params, {
run: { cmd: "execute", args: ["build"] },
});
This composes safely even when a run (or any other) binding already exists in the template scope.
Verified by building and running a minimal reproduction with Go 1.26 and Node 22.
**1. Bug reproduced exactly as reported (before fix):**
```
$ ./dagger # bare invocation
panic: runtime error: slice bounds out of range [2:1]
goroutine 1 [running]:
main.main()
buggy/main.go:27 +0x1b0
exit=2
```
**2. Fixed binary — edge cases (all passed):**
| Invocation | Result | Exit |
|---|---|---|
| `dagger` (bare) | usage printed, no panic | 2 |
| `dagger run` | `dagger run: args=[]` | 0 |
| `dagger run a b c` | `dagger run: args=[a b c]` | 0 |
| `dagger help` / `-h` / `--help` | usage printed | 0 |
| `dagger bogus` | `dagger: unknown command "bogus"` + usage | 2 |
**3. Templates params injection (Node vm test):**
```
buggy -> SyntaxError: Identifier 'run' has already been declared
fixed -> params: {"hello":"world","run":{"cmd":"execute","args":["build"]}}
fixed -> params.run: {"cmd":"execute","args":["build"]}
fixed -> no SyntaxError, run subcommand available: true
```
No panics, no SyntaxError; bare invocation degrades to a clean usage message.
---{"model": "deepseek-v4-flash", "problem_class": "go-cli-bare-invocation-panic", "result": "passed", "tests": 8}