How to Contribute New Commands to the Gastown CLI via the cmd Directory
TLDR: You contribute new commands by creating Go source files in the internal/cmd/ directory that define a Cobra command struct, implement the logic in a RunE closure, and register the command to the root command via an init() function.
The gastownhall/gastown project provides a command-line interface built on the popular Cobra framework. To extend the CLI’s functionality, developers add self-contained command files to the internal/cmd package, following a consistent registration pattern that keeps the codebase modular and testable.
Understanding the Gastown CLI Architecture
The CLI follows a clean separation between the entry point and command implementations, making it straightforward to add new functionality without touching core bootstrap logic.
The Entry Point in cmd/gt/main.go
The binary starts at cmd/gt/main.go, which serves as a minimal wrapper. According to the source code, this file simply imports the internal/cmd package and calls cmd.Execute() to bootstrap the application:
// cmd/gt/main.go
package main
import (
"os"
"github.com/gastownhall/gastown/internal/cmd"
)
func main() {
os.Exit(cmd.Execute())
}
The internal/cmd Package Structure
The internal/cmd directory contains the root command definition and all sub-commands. The central bootstrap lives in the file defining func Execute() (typically root.go), which initializes the global rootCmd variable. Every individual sub-command is a separate Go file that creates a *cobra.Command and attaches it to this root during package initialization.
Step-by-Step Guide to Contributing a New Command
Follow these steps to add a command like gt hello to the gastownhall/gastown project.
1. Clone and Verify the Build
Start by cloning the repository and ensuring the existing test suite passes:
git clone https://github.com/gastownhall/gastown.git
cd gastown
go test ./...
2. Create the Command File
Add a new file at internal/cmd/<name>.go. For example, create internal/cmd/hello.go to implement a gt hello command.
3. Define the Cobra Command
Implement a constructor function that returns a *cobra.Command with the Use, Short, and RunE fields configured. The RunE closure contains your command’s business logic:
// internal/cmd/hello.go
package cmd
import (
"github.com/spf13/cobra"
)
// NewHelloCmd creates the `hello` sub-command.
func NewHelloCmd() *cobra.Command {
return &cobra.Command{
Use: "hello",
Short: "Print a friendly greeting",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Println("👋 Hello, Gastown!")
return nil
},
}
}
4. Register with the Root Command
Add an init() function that calls rootCmd.AddCommand() to attach your command to the CLI tree:
func init() {
rootCmd.AddCommand(NewHelloCmd())
}
5. Write Unit Tests
Create internal/cmd/hello_test.go to verify execution. Capture stdout and assert on the output:
// internal/cmd/hello_test.go
package cmd
import (
"bytes"
"testing"
"github.com/spf13/cobra"
)
func TestHelloCommand(t *testing.T) {
buf := new(bytes.Buffer)
root := &cobra.Command{Use: "gt"}
root.AddCommand(NewHelloCmd())
root.SetOut(buf)
root.SetArgs([]string{"hello"})
if err := root.Execute(); err != nil {
t.Fatalf("command failed: %v", err)
}
got := buf.String()
want := "👋 Hello, Gastown!\n"
if got != want {
t.Fatalf("unexpected output: got %q, want %q", got, want)
}
}
6. Validate and Submit
Run the full test suite to ensure no regressions, then commit your changes and open a pull request:
go test ./...
Key Files and Conventions
When contributing to the gastownhall/gastown project, reference these critical files:
| File | Role |
|---|---|
cmd/gt/main.go |
Entry point that calls cmd.Execute() |
internal/cmd/root.go |
Defines func Execute() and the global rootCmd variable |
internal/cmd/status.go |
Existing command example showing the standard Cobra pattern |
internal/cmd/hello.go |
Your new command implementation (to be added) |
internal/cmd/hello_test.go |
Corresponding unit tests (to be added) |
Summary
- Place new commands in
internal/cmd/as separate.gofiles to maintain modularity. - Use the Cobra framework by creating a
*cobra.Commandwith aRunEclosure for error handling. - Register automatically via an
init()function that callsrootCmd.AddCommand(). - Never modify
cmd/gt/main.go; the existingExecute()bootstrap handles all sub-commands dynamically. - Always include tests that execute the command through Cobra’s API and verify output buffers.
Frequently Asked Questions
Where do I place new command files in the Gastown repository?
Create them in the internal/cmd/ directory. The gastownhall/gastown project uses this package to house all CLI logic. Each command gets its own file (e.g., hello.go) alongside a corresponding test file (hello_test.go).
Do I need to modify cmd/gt/main.go to add a new subcommand?
No. The entry point at cmd/gt/main.go only calls cmd.Execute(). Because Cobra commands register themselves via init() functions in the internal/cmd package, simply adding a new file to that directory automatically includes it in the CLI tree.
How does the Cobra framework handle command registration in Gastown?
Registration happens at package initialization time. Each command file defines an init() function that calls rootCmd.AddCommand() with the command constructor (e.g., NewHelloCmd()). When Go loads the internal/cmd package, all init() functions execute, building the complete command tree before Execute() runs.
What testing pattern should I follow for new CLI commands?
Create a test file in internal/cmd/ that instantiates the root command, adds your new command via its constructor, and sets a bytes.Buffer to capture stdout. Use root.SetArgs() to specify command-line arguments, then call root.Execute() and assert on the captured output. This pattern validates both the command registration and the business logic without requiring a compiled binary.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →